diff --git a/models/server-activity/src/index.ts b/models/server-activity/src/index.ts index 72530fd081..5d4820a247 100644 --- a/models/server-activity/src/index.ts +++ b/models/server-activity/src/index.ts @@ -44,13 +44,11 @@ export function createModel (builder: Builder): void { builder.createDoc(serverCore.class.Trigger, core.space.Model, { trigger: serverActivity.trigger.ActivityMessagesHandler, - arrays: true, isAsync: true }) builder.createDoc(serverCore.class.Trigger, core.space.Model, { - trigger: serverActivity.trigger.OnDocRemoved, - arrays: true + trigger: serverActivity.trigger.OnDocRemoved }) builder.createDoc(serverCore.class.Trigger, core.space.Model, { diff --git a/models/server-ai-bot/src/index.ts b/models/server-ai-bot/src/index.ts index b57e134b5c..7fd057182d 100644 --- a/models/server-ai-bot/src/index.ts +++ b/models/server-ai-bot/src/index.ts @@ -37,7 +37,6 @@ export function createModel (builder: Builder): void { builder.createDoc(serverCore.class.Trigger, core.space.Model, { trigger: serverAiBot.trigger.OnMessageSend, - arrays: true, isAsync: true }) diff --git a/models/server-collaboration/src/index.ts b/models/server-collaboration/src/index.ts index 1e7b7d8077..30e6796737 100644 --- a/models/server-collaboration/src/index.ts +++ b/models/server-collaboration/src/index.ts @@ -23,7 +23,6 @@ export { serverCollaborationId } from '@hcengineering/server-collaboration' export function createModel (builder: Builder): void { builder.createDoc(serverCore.class.Trigger, core.space.Model, { - trigger: serverCollaboration.trigger.OnDelete, - arrays: true + trigger: serverCollaboration.trigger.OnDelete }) } diff --git a/models/server-fulltext/src/index.ts b/models/server-fulltext/src/index.ts index b9716d0b8a..0a679bd55b 100644 --- a/models/server-fulltext/src/index.ts +++ b/models/server-fulltext/src/index.ts @@ -21,7 +21,6 @@ import serverFulltext from '@hcengineering/server-fulltext' export { serverFulltextId } from '@hcengineering/server-fulltext' export function createModel (builder: Builder): void { builder.createDoc(serverCore.class.Trigger, core.space.Model, { - trigger: serverFulltext.trigger.OnChange, - arrays: true + trigger: serverFulltext.trigger.OnChange }) } diff --git a/models/server-notification/src/index.ts b/models/server-notification/src/index.ts index c337b396d7..7a84045db4 100644 --- a/models/server-notification/src/index.ts +++ b/models/server-notification/src/index.ts @@ -89,8 +89,7 @@ export function createModel (builder: Builder): void { }) builder.createDoc(serverCore.class.Trigger, core.space.Model, { - trigger: serverNotification.trigger.OnDocRemove, - arrays: true + trigger: serverNotification.trigger.OnDocRemove }) builder.createDoc(serverCore.class.Trigger, core.space.Model, { diff --git a/models/server-request/src/index.ts b/models/server-request/src/index.ts index 50ecf97463..007cceb17d 100644 --- a/models/server-request/src/index.ts +++ b/models/server-request/src/index.ts @@ -25,8 +25,7 @@ export { serverRequestId } from '@hcengineering/server-request' export function createModel (builder: Builder): void { builder.createDoc(serverCore.class.Trigger, core.space.Model, { - trigger: serverRequest.trigger.OnRequest, - arrays: true + trigger: serverRequest.trigger.OnRequest }) builder.mixin(request.class.Request, core.class.Class, serverNotification.mixin.TextPresenter, { diff --git a/models/server-tags/src/index.ts b/models/server-tags/src/index.ts index 456b0a5315..842b1d05d7 100644 --- a/models/server-tags/src/index.ts +++ b/models/server-tags/src/index.ts @@ -23,8 +23,7 @@ export { serverTagsId } from '@hcengineering/server-tags' export function createModel (builder: Builder): void { builder.createDoc(serverCore.class.Trigger, core.space.Model, { - trigger: serverTags.trigger.onTagReference, - arrays: true + trigger: serverTags.trigger.onTagReference }) builder.mixin, ObjectDDParticipant>( diff --git a/models/server-task/src/index.ts b/models/server-task/src/index.ts index c02b5d2f67..80e2227a88 100644 --- a/models/server-task/src/index.ts +++ b/models/server-task/src/index.ts @@ -22,7 +22,6 @@ export { serverTaskId } from '@hcengineering/server-task' export function createModel (builder: Builder): void { builder.createDoc(serverCore.class.Trigger, core.space.Model, { - trigger: serverTask.trigger.OnStateUpdate, - arrays: true + trigger: serverTask.trigger.OnStateUpdate }) } diff --git a/models/server-time/src/index.ts b/models/server-time/src/index.ts index 16266df90d..152eacf9f0 100644 --- a/models/server-time/src/index.ts +++ b/models/server-time/src/index.ts @@ -38,7 +38,6 @@ export function createModel (builder: Builder): void { builder.createDoc(serverCore.class.Trigger, core.space.Model, { trigger: serverTime.trigger.OnTask, - arrays: true, isAsync: true }) diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 5feac91b64..735a0b0ba5 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -13,6 +13,7 @@ // limitations under the License. // +import { Analytics } from '@hcengineering/analytics' import { BackupClient, DocChunk } from './backup' import { Account, AttachedDoc, Class, DOMAIN_MODEL, Doc, Domain, Ref, Timestamp } from './classes' import core from './component' @@ -249,12 +250,10 @@ export async function createClient ( } lastTx = tx.reduce((cur, it) => (it.modifiedOn > cur ? it.modifiedOn : cur), 0) } - const conn = await ctx.with('connect', {}, async () => await connect(txHandler)) + const conn = await ctx.with('connect', {}, () => connect(txHandler)) - await ctx.with( - 'load-model', - { reload: false }, - async (ctx) => await loadModel(ctx, conn, modelFilter, hierarchy, model, false, txPersistence) + await ctx.with('load-model', { reload: false }, (ctx) => + loadModel(ctx, conn, modelFilter, hierarchy, model, false, txPersistence) ) txBuffer = txBuffer.filter((tx) => tx.space !== core.space.Model) @@ -273,10 +272,8 @@ export async function createClient ( return } // Find all new transactions and apply - const loadModelResponse = await ctx.with( - 'connect', - { reload: true }, - async (ctx) => await loadModel(ctx, conn, modelFilter, hierarchy, model, true, txPersistence) + const loadModelResponse = await ctx.with('connect', { reload: true }, (ctx) => + loadModel(ctx, conn, modelFilter, hierarchy, model, true, txPersistence) ) if (event === ClientConnectEvent.Reconnected && loadModelResponse.full) { @@ -284,9 +281,7 @@ export async function createClient ( hierarchy = new Hierarchy() model = new ModelDb(hierarchy) - await ctx.with('build-model', {}, async (ctx) => { - await buildModel(ctx, loadModelResponse, modelFilter, hierarchy, model) - }) + await ctx.with('build-model', {}, (ctx) => buildModel(ctx, loadModelResponse, modelFilter, hierarchy, model)) await oldOnConnect?.(ClientConnectEvent.Upgraded) // No need to fetch more stuff since upgrade was happened. @@ -300,15 +295,12 @@ export async function createClient ( } // We need to look for last {transactionThreshold} transactions and if it is more since lastTx one we receive, we need to perform full refresh. - const atxes = await ctx.with( - 'find-atx', - {}, - async () => - await conn.findAll( - core.class.Tx, - { modifiedOn: { $gt: lastTx }, objectSpace: { $ne: core.space.Model } }, - { sort: { modifiedOn: SortingOrder.Ascending, _id: SortingOrder.Ascending }, limit: transactionThreshold } - ) + const atxes = await ctx.with('find-atx', {}, () => + conn.findAll( + core.class.Tx, + { modifiedOn: { $gt: lastTx }, objectSpace: { $ne: core.space.Model } }, + { sort: { modifiedOn: SortingOrder.Ascending, _id: SortingOrder.Ascending }, limit: transactionThreshold } + ) ) let needFullRefresh = false @@ -364,12 +356,16 @@ async function tryLoadModel ( } // Save concatenated - void (await ctx.with('persistence-store', {}, (ctx) => - persistence?.store({ - ...result, - transactions: !result.full ? current.transactions.concat(result.transactions) : result.transactions + void ctx + .with('persistence-store', {}, (ctx) => + persistence?.store({ + ...result, + transactions: !result.full ? current.transactions.concat(result.transactions) : result.transactions + }) + ) + .catch((err) => { + Analytics.handleError(err) }) - )) if (!result.full && !reload) { result.transactions = current.transactions.concat(result.transactions) @@ -432,7 +428,7 @@ async function buildModel ( const atxes = modelResponse.transactions - await ctx.with('split txes', {}, async () => { + ctx.withSync('split txes', {}, () => { atxes.forEach((tx) => ((tx.modifiedBy === core.account.ConfigUser || tx.modifiedBy === core.account.System) && !isPersonAccount(tx) ? systemTx @@ -448,7 +444,7 @@ async function buildModel ( txes = await modelFilter(txes) } - await ctx.with('build hierarchy', {}, async () => { + ctx.withSync('build hierarchy', {}, () => { for (const tx of txes) { try { hierarchy.tx(tx) @@ -461,7 +457,7 @@ async function buildModel ( } } }) - await ctx.with('build model', {}, async (ctx) => { + ctx.withSync('build model', {}, (ctx) => { model.addTxes(ctx, txes, false) }) } diff --git a/packages/core/src/measurements/context.ts b/packages/core/src/measurements/context.ts index f6e3bddc98..e5dadc7e23 100644 --- a/packages/core/src/measurements/context.ts +++ b/packages/core/src/measurements/context.ts @@ -47,6 +47,8 @@ const consoleLogger = (logParams: Record): MeasureLogger => ({ const noParamsLogger = consoleLogger({}) +const nullPromise = Promise.resolve() + /** * @public */ @@ -148,6 +150,9 @@ export class MeasureMetricsContext implements MeasureContext { c.end() }) } else { + if (value == null) { + return nullPromise as Promise + } return Promise.resolve(value) } } finally { diff --git a/packages/core/src/measurements/metrics.ts b/packages/core/src/measurements/metrics.ts index 516e0b83d5..49d6591804 100644 --- a/packages/core/src/measurements/metrics.ts +++ b/packages/core/src/measurements/metrics.ts @@ -104,8 +104,8 @@ export function updateMeasure ( param.value += value ?? ed - st param.operations++ } - - param.topResult = getUpdatedTopResult(param.topResult, ed - st, fParams) + // Do not update top results for params. + // param.topResult = getUpdatedTopResult(param.topResult, ed - st, fParams) } // Update leaf data if (override === true) { diff --git a/packages/core/src/server.ts b/packages/core/src/server.ts index 130f651a00..ed3a1589d4 100644 --- a/packages/core/src/server.ts +++ b/packages/core/src/server.ts @@ -13,7 +13,7 @@ // limitations under the License. // -import type { Account, Doc, Domain, Ref } from './classes' +import type { Account, Doc, DocIndexState, Domain, Ref } from './classes' import { MeasureContext } from './measurements' import { DocumentQuery, FindOptions } from './storage' import type { DocumentUpdate, Tx } from './tx' @@ -58,7 +58,7 @@ export interface SessionData { workspace: WorkspaceIdWithUrl branding: Branding | null - needWarmupFulltext?: boolean + fulltextUpdates?: Map, DocIndexState> } /** diff --git a/packages/query/src/index.ts b/packages/query/src/index.ts index c013743957..33ce6edd2d 100644 --- a/packages/query/src/index.ts +++ b/packages/query/src/index.ts @@ -1317,84 +1317,73 @@ export class LiveQuery implements WithTx, Client { return result } - triggerInProgress = true - private async checkUpdateEvents (evt: TxWorkspaceEvent, trigger = true): Promise { - if (this.triggerInProgress) { - this.triggerInProgress = false - const h = this.client.getHierarchy() - function hasClass (q: Query, classes: Ref>[]): boolean { - return ( - classes.includes(q._class) || classes.some((it) => h.isDerived(q._class, it) || h.isDerived(it, q._class)) - ) + const h = this.client.getHierarchy() + function hasClass (q: Query, classes: Ref>[]): boolean { + return classes.includes(q._class) || classes.some((it) => h.isDerived(q._class, it) || h.isDerived(it, q._class)) + } + if (evt.event === WorkspaceEvent.IndexingUpdate) { + const indexingParam = evt.params as IndexingUpdateEvent + for (const q of [...this.queue]) { + if (hasClass(q, indexingParam._class) && q.query.$search !== undefined) { + if (!this.removeFromQueue(q)) { + try { + await this.refresh(q) + } catch (err: any) { + Analytics.handleError(err) + console.error(err) + } + } else { + const queries = this.queries.get(q._class) + const pos = queries?.indexOf(q) ?? -1 + if (pos >= 0 && queries !== undefined) { + queries.splice(pos, 1) + if (queries?.length === 0) { + this.queries.delete(q._class) + } + } + } + } } - if (evt.event === WorkspaceEvent.IndexingUpdate) { - const indexingParam = evt.params as IndexingUpdateEvent - for (const q of [...this.queue]) { + for (const v of this.queries.values()) { + for (const q of v) { if (hasClass(q, indexingParam._class) && q.query.$search !== undefined) { - if (!this.removeFromQueue(q)) { - try { - await this.refresh(q) - } catch (err: any) { - Analytics.handleError(err) - console.error(err) - } - } else { - const queries = this.queries.get(q._class) - const pos = queries?.indexOf(q) ?? -1 - if (pos >= 0 && queries !== undefined) { - queries.splice(pos, 1) - if (queries?.length === 0) { - this.queries.delete(q._class) - } - } - } - } - } - for (const v of this.queries.values()) { - for (const q of v) { - if (hasClass(q, indexingParam._class) && q.query.$search !== undefined) { - try { - await this.refresh(q) - } catch (err: any) { - Analytics.handleError(err) - console.error(err) - } + try { + await this.refresh(q) + } catch (err: any) { + Analytics.handleError(err) + console.error(err) } } } } - if (evt.event === WorkspaceEvent.BulkUpdate) { - const params = evt.params as BulkUpdateEvent - for (const q of [...this.queue]) { + } + if (evt.event === WorkspaceEvent.BulkUpdate) { + const params = evt.params as BulkUpdateEvent + for (const q of [...this.queue]) { + if (hasClass(q, params._class)) { + if (!this.removeFromQueue(q)) { + try { + await this.refresh(q) + } catch (err: any) { + Analytics.handleError(err) + console.error(err) + } + } + } + } + for (const v of this.queries.values()) { + for (const q of v) { if (hasClass(q, params._class)) { - if (!this.removeFromQueue(q)) { - try { - await this.refresh(q) - } catch (err: any) { - Analytics.handleError(err) - console.error(err) - } - } - } - } - for (const v of this.queries.values()) { - for (const q of v) { - if (hasClass(q, params._class)) { - try { - await this.refresh(q) - } catch (err: any) { - Analytics.handleError(err) - console.error(err) - } + try { + await this.refresh(q) + } catch (err: any) { + Analytics.handleError(err) + console.error(err) } } } } - setTimeout(() => { - this.triggerInProgress = true - void this.checkUpdateEvents(evt, false) - }, 20000) } } diff --git a/pods/stats/src/stats.ts b/pods/stats/src/stats.ts index 1eb58cd2ca..ee7d158287 100644 --- a/pods/stats/src/stats.ts +++ b/pods/stats/src/stats.ts @@ -62,7 +62,11 @@ export function serveStats (ctx: MeasureContext, onClose?: () => void): void { credentials: true }) ) - app.use(bodyParser()) + app.use( + bodyParser({ + jsonLimit: '150mb' + }) + ) router.get('/api/v1/overview', (req, res) => { try { diff --git a/server-plugins/activity-resources/src/index.ts b/server-plugins/activity-resources/src/index.ts index 5811c291f3..b53524d044 100644 --- a/server-plugins/activity-resources/src/index.ts +++ b/server-plugins/activity-resources/src/index.ts @@ -53,21 +53,23 @@ import { import { ReferenceTrigger } from './references' import { getAttrName, getCollectionAttribute, getDocUpdateAction, getTxAttributesUpdates } from './utils' -export async function OnReactionChanged (originTx: Tx, control: TriggerControl): Promise { - const tx = originTx as TxCollectionCUD - const innerTx = TxProcessor.extractTx(tx) as TxCUD +export async function OnReactionChanged (txes: Tx[], control: TriggerControl): Promise { + for (const originTx of txes) { + const tx = originTx as TxCollectionCUD + const innerTx = TxProcessor.extractTx(tx) as TxCUD - if (innerTx._class === core.class.TxCreateDoc) { - const txes = await createReactionNotifications(tx, control) + if (innerTx._class === core.class.TxCreateDoc) { + const txes = await createReactionNotifications(tx, control) - await control.apply(control.ctx, txes) - return [] - } + await control.apply(control.ctx, txes) + continue + } - if (innerTx._class === core.class.TxRemoveDoc) { - const txes = await removeReactionNotifications(tx, control) - await control.apply(control.ctx, txes) - return [] + if (innerTx._class === core.class.TxRemoveDoc) { + const txes = await removeReactionNotifications(tx, control) + await control.apply(control.ctx, txes) + continue + } } return [] @@ -304,11 +306,8 @@ export async function generateDocUpdateMessages ( switch (tx._class) { case core.class.TxCreateDoc: { const doc = TxProcessor.createDoc2Doc(tx as TxCreateDoc) - return await ctx.with( - 'pushDocUpdateMessages', - {}, - async (ctx) => - await pushDocUpdateMessages(ctx, control, res, doc, originTx ?? tx, undefined, objectCache, controlRules) + return await ctx.with('pushDocUpdateMessages', {}, (ctx) => + pushDocUpdateMessages(ctx, control, res, doc, originTx ?? tx, undefined, objectCache, controlRules) ) } case core.class.TxMixin: @@ -322,20 +321,8 @@ export async function generateDocUpdateMessages ( doc = (await control.findAll(ctx, tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0] objectCache?.docs?.set(tx.objectId, doc) } - return await ctx.with( - 'pushDocUpdateMessages', - {}, - async (ctx) => - await pushDocUpdateMessages( - ctx, - control, - res, - doc ?? undefined, - originTx ?? tx, - undefined, - objectCache, - controlRules - ) + return await ctx.with('pushDocUpdateMessages', {}, (ctx) => + pushDocUpdateMessages(ctx, control, res, doc ?? undefined, originTx ?? tx, undefined, objectCache, controlRules) ) } case core.class.TxCollectionCUD: { @@ -363,20 +350,17 @@ export async function generateDocUpdateMessages ( } if (doc !== undefined) { objectCache?.docs?.set(tx.objectId, doc) - return await ctx.with( - 'pushDocUpdateMessages', - {}, - async (ctx) => - await pushDocUpdateMessages( - ctx, - control, - res, - doc ?? undefined, - originTx ?? tx, - undefined, - objectCache, - controlRules - ) + return await ctx.with('pushDocUpdateMessages', {}, (ctx) => + pushDocUpdateMessages( + ctx, + control, + res, + doc ?? undefined, + originTx ?? tx, + undefined, + objectCache, + controlRules + ) ) } } diff --git a/server-plugins/activity-resources/src/references.ts b/server-plugins/activity-resources/src/references.ts index 3a40701309..e1f75a07ba 100644 --- a/server-plugins/activity-resources/src/references.ts +++ b/server-plugins/activity-resources/src/references.ts @@ -791,21 +791,27 @@ async function ActivityReferenceRemove (tx: Tx, etx: TxCUD, control: Trigge /** * @public */ -export async function ReferenceTrigger (tx: TxCUD, control: TriggerControl): Promise { +export async function ReferenceTrigger (txes: TxCUD[], control: TriggerControl): Promise { const result: Tx[] = [] - const etx = TxProcessor.extractTx(tx) as TxCUD - if (control.hierarchy.isDerived(etx.objectClass, activity.class.ActivityReference)) return [] - if (control.hierarchy.isDerived(etx.objectClass, notification.class.InboxNotification)) return [] + for (const tx of txes) { + const etx = TxProcessor.extractTx(tx) as TxCUD + if (control.hierarchy.isDerived(etx.objectClass, activity.class.ActivityReference)) { + continue + } + if (control.hierarchy.isDerived(etx.objectClass, notification.class.InboxNotification)) { + continue + } - if (etx._class === core.class.TxCreateDoc) { - result.push(...(await ActivityReferenceCreate(tx, etx, control))) - } - if (etx._class === core.class.TxUpdateDoc) { - result.push(...(await ActivityReferenceUpdate(tx, etx, control))) - } - if (etx._class === core.class.TxRemoveDoc) { - result.push(...(await ActivityReferenceRemove(tx, etx, control))) + if (etx._class === core.class.TxCreateDoc) { + result.push(...(await ActivityReferenceCreate(tx, etx, control))) + } + if (etx._class === core.class.TxUpdateDoc) { + result.push(...(await ActivityReferenceUpdate(tx, etx, control))) + } + if (etx._class === core.class.TxRemoveDoc) { + result.push(...(await ActivityReferenceRemove(tx, etx, control))) + } } return result } diff --git a/server-plugins/ai-bot-resources/src/index.ts b/server-plugins/ai-bot-resources/src/index.ts index 6f558240d5..ee9b08d7ae 100644 --- a/server-plugins/ai-bot-resources/src/index.ts +++ b/server-plugins/ai-bot-resources/src/index.ts @@ -278,7 +278,7 @@ export async function OnMessageSend ( return [] } -export async function OnMention (tx: TxCreateDoc, control: TriggerControl): Promise { +export async function OnMention (tx: TxCreateDoc[], control: TriggerControl): Promise { // Note: temporally commented until open ai will be added // if (tx.objectClass !== notification.class.MentionInboxNotification || tx._class !== core.class.TxCreateDoc) { // return [] @@ -308,7 +308,7 @@ export async function OnMention (tx: TxCreateDoc, cont } export async function OnMessageNotified ( - tx: TxCreateDoc, + tx: TxCreateDoc[], control: TriggerControl ): Promise { // Note: temporally commented until open ai will be added @@ -363,45 +363,47 @@ export async function OnMessageNotified ( return [] } -export async function OnUserStatus (originTx: Tx, control: TriggerControl): Promise { - const tx = TxProcessor.extractTx(originTx) as TxCUD +export async function OnUserStatus (txes: Tx[], control: TriggerControl): Promise { + for (const originTx of txes) { + const tx = TxProcessor.extractTx(originTx) as TxCUD - if ( - tx.objectClass !== core.class.UserStatus || - ![core.class.TxCreateDoc, core.class.TxUpdateDoc].includes(tx._class) - ) { - return [] - } - - if (tx._class === core.class.TxCreateDoc) { - const createTx = tx as TxCreateDoc - const status = TxProcessor.createDoc2Doc(createTx) - if (status.user === aiBot.account.AIBot || status.user === core.account.System || !status.online) { - return [] - } - } - - if (tx._class === core.class.TxUpdateDoc) { - const updateTx = tx as TxUpdateDoc - const val = updateTx.operations.online - if (val !== true) { - return [] + if ( + tx.objectClass !== core.class.UserStatus || + ![core.class.TxCreateDoc, core.class.TxUpdateDoc].includes(tx._class) + ) { + continue } - const status = (await control.findAll(control.ctx, core.class.UserStatus, { _id: updateTx.objectId }))[0] - if (status === undefined || status.user === aiBot.account.AIBot || status.user === core.account.System) { - return [] + if (tx._class === core.class.TxCreateDoc) { + const createTx = tx as TxCreateDoc + const status = TxProcessor.createDoc2Doc(createTx) + if (status.user === aiBot.account.AIBot || status.user === core.account.System || !status.online) { + continue + } } + + if (tx._class === core.class.TxUpdateDoc) { + const updateTx = tx as TxUpdateDoc + const val = updateTx.operations.online + if (val !== true) { + continue + } + + const status = (await control.findAll(control.ctx, core.class.UserStatus, { _id: updateTx.objectId }))[0] + if (status === undefined || status.user === aiBot.account.AIBot || status.user === core.account.System) { + continue + } + } + + const account = control.modelDb.findAllSync(contact.class.PersonAccount, { email: aiBotAccountEmail })[0] + + if (account !== undefined) { + continue + } + + await createAccountRequest(control.workspace, control.ctx) } - const account = control.modelDb.findAllSync(contact.class.PersonAccount, { email: aiBotAccountEmail })[0] - - if (account !== undefined) { - return [] - } - - await createAccountRequest(control.workspace, control.ctx) - return [] } diff --git a/server-plugins/attachment-resources/src/index.ts b/server-plugins/attachment-resources/src/index.ts index b277437f92..553ca560f4 100644 --- a/server-plugins/attachment-resources/src/index.ts +++ b/server-plugins/attachment-resources/src/index.ts @@ -23,19 +23,24 @@ import type { TriggerControl } from '@hcengineering/server-core' * @public */ export async function OnAttachmentDelete ( - tx: Tx, + txes: Tx[], { removedMap, ctx, storageAdapter, workspace }: TriggerControl ): Promise { - const rmTx = TxProcessor.extractTx(tx) as TxRemoveDoc + const toDelete: string[] = [] + for (const tx of txes) { + const rmTx = TxProcessor.extractTx(tx) as TxRemoveDoc - // Obtain document being deleted. - const attach = removedMap.get(rmTx.objectId) as Attachment + // Obtain document being deleted. + const attach = removedMap.get(rmTx.objectId) as Attachment - if (attach === undefined) { - return [] + if (attach === undefined) { + continue + } + toDelete.push(attach.file) + } + if (toDelete.length > 0) { + await storageAdapter.remove(ctx, workspace, toDelete) } - - await storageAdapter.remove(ctx, workspace, [attach.file]) return [] } diff --git a/server-plugins/calendar-resources/src/index.ts b/server-plugins/calendar-resources/src/index.ts index ef2b0ac888..32293b898e 100644 --- a/server-plugins/calendar-resources/src/index.ts +++ b/server-plugins/calendar-resources/src/index.ts @@ -83,23 +83,28 @@ export async function ReminderTextPresenter (doc: Doc, control: TriggerControl): /** * @public */ -export async function OnPersonAccountCreate (tx: Tx, control: TriggerControl): Promise { - const ctx = TxProcessor.extractTx(tx) as TxCreateDoc - const user = TxProcessor.createDoc2Doc(ctx) +export async function OnPersonAccountCreate (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + const ctx = TxProcessor.extractTx(tx) as TxCreateDoc + const user = TxProcessor.createDoc2Doc(ctx) - const res: TxCreateDoc = control.txFactory.createTxCreateDoc( - calendar.class.Calendar, - calendar.space.Calendar, - { - name: user.email, - hidden: false, - visibility: 'public' - }, - `${user._id}_calendar` as Ref, - undefined, - user._id - ) - return [res] + result.push( + control.txFactory.createTxCreateDoc( + calendar.class.Calendar, + calendar.space.Calendar, + { + name: user.email, + hidden: false, + visibility: 'public' + }, + `${user._id}_calendar` as Ref, + undefined, + user._id + ) + ) + } + return result } function getCalendar (calendars: Calendar[], person: Ref): Ref | undefined { @@ -118,17 +123,20 @@ function getEventPerson (current: Event, calendars: Calendar[], control: Trigger return acc.person } -async function OnEvent (tx: Tx, control: TriggerControl): Promise { - const ctx = TxProcessor.extractTx(tx) as TxCUD - if (ctx._class === core.class.TxCreateDoc) { - return await onEventCreate(ctx as TxCreateDoc, control) - } else if (ctx._class === core.class.TxUpdateDoc) { - return await onEventUpdate(ctx as TxUpdateDoc, control) - } else if (ctx._class === core.class.TxRemoveDoc) { - return await onRemoveEvent(ctx as TxRemoveDoc, control) +async function OnEvent (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + const ctx = TxProcessor.extractTx(tx) as TxCUD + if (ctx._class === core.class.TxCreateDoc) { + result.push(...(await onEventCreate(ctx as TxCreateDoc, control))) + } else if (ctx._class === core.class.TxUpdateDoc) { + result.push(...(await onEventUpdate(ctx as TxUpdateDoc, control))) + } else if (ctx._class === core.class.TxRemoveDoc) { + result.push(...(await onRemoveEvent(ctx as TxRemoveDoc, control))) + } } - return [] + return result } async function onEventUpdate (ctx: TxUpdateDoc, control: TriggerControl): Promise { diff --git a/server-plugins/chunter-resources/src/index.ts b/server-plugins/chunter-resources/src/index.ts index 76e99455b6..d3a0ef59b5 100644 --- a/server-plugins/chunter-resources/src/index.ts +++ b/server-plugins/chunter-resources/src/index.ts @@ -203,16 +203,20 @@ async function OnChatMessageCreated (ctx: MeasureContext, tx: TxCUD, contro return res } -async function ChatNotificationsHandler (tx: TxCUD, control: TriggerControl): Promise { - const actualTx = TxProcessor.extractTx(tx) as TxCreateDoc +async function ChatNotificationsHandler (txes: TxCUD[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + const actualTx = TxProcessor.extractTx(tx) as TxCreateDoc - if (actualTx._class !== core.class.TxCreateDoc) { - return [] + if (actualTx._class !== core.class.TxCreateDoc) { + continue + } + + const chatMessage = TxProcessor.createDoc2Doc(actualTx) + + result.push(...(await createCollaboratorNotifications(control.ctx, tx, control, [chatMessage]))) } - - const chatMessage = TxProcessor.createDoc2Doc(actualTx) - - return await createCollaboratorNotifications(control.ctx, tx, control, [chatMessage]) + return result } function joinChannel (control: TriggerControl, channel: Channel, user: Ref): Tx[] { @@ -263,27 +267,29 @@ async function OnThreadMessageDeleted (tx: Tx, control: TriggerControl): Promise /** * @public */ -export async function ChunterTrigger (tx: TxCUD, control: TriggerControl): Promise { +export async function ChunterTrigger (txes: TxCUD[], control: TriggerControl): Promise { const res: Tx[] = [] - const actualTx = TxProcessor.extractTx(tx) as TxCreateDoc + for (const tx of txes) { + const actualTx = TxProcessor.extractTx(tx) as TxCreateDoc - if ( - actualTx._class === core.class.TxCreateDoc && - control.hierarchy.isDerived(actualTx.objectClass, chunter.class.ThreadMessage) - ) { - 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', {}, (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', {}, (ctx) => OnChatMessageCreated(ctx, tx, control)))) + if ( + actualTx._class === core.class.TxCreateDoc && + control.hierarchy.isDerived(actualTx.objectClass, chunter.class.ThreadMessage) + ) { + 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', {}, (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', {}, (ctx) => OnChatMessageCreated(ctx, tx, control)))) + } } return res } @@ -342,20 +348,21 @@ export async function getChunterNotificationContent ( } } -async function OnChatMessageRemoved (tx: TxCollectionCUD, control: TriggerControl): Promise { - if (tx.tx._class !== core.class.TxRemoveDoc) { - return [] - } - +async function OnChatMessageRemoved (txes: TxCollectionCUD[], control: TriggerControl): Promise { const res: Tx[] = [] - const notifications = await control.findAll(control.ctx, notification.class.InboxNotification, { - attachedTo: tx.tx.objectId - }) + for (const tx of txes) { + if (tx.tx._class !== core.class.TxRemoveDoc) { + continue + } - notifications.forEach((notification) => { - res.push(control.txFactory.createTxRemoveDoc(notification._class, notification.space, notification._id)) - }) + const notifications = await control.findAll(control.ctx, notification.class.InboxNotification, { + attachedTo: tx.tx.objectId + }) + notifications.forEach((notification) => { + res.push(control.txFactory.createTxRemoveDoc(notification._class, notification.space, notification._id)) + }) + } return res } @@ -470,22 +477,26 @@ export async function syncChat (control: TriggerControl, status: UserStatus, dat await control.apply(control.ctx, res, true) } -async function OnUserStatus (originTx: TxCUD, control: TriggerControl): Promise { - const tx = TxProcessor.extractTx(originTx) as TxCUD - if (tx.objectClass !== core.class.UserStatus) return [] - if (tx._class === core.class.TxCreateDoc) { - const createTx = tx as TxCreateDoc - const { online } = createTx.attributes - if (online) { - const status = TxProcessor.createDoc2Doc(createTx) - await syncChat(control, status, originTx.modifiedOn) +async function OnUserStatus (txes: TxCUD[], control: TriggerControl): Promise { + for (const originTx of txes) { + const tx = TxProcessor.extractTx(originTx) as TxCUD + if (tx.objectClass !== core.class.UserStatus) { + continue } - } else if (tx._class === core.class.TxUpdateDoc) { - const updateTx = tx as TxUpdateDoc - const { online } = updateTx.operations - if (online === true) { - const status = (await control.findAll(control.ctx, core.class.UserStatus, { _id: updateTx.objectId }))[0] - await syncChat(control, status, originTx.modifiedOn) + if (tx._class === core.class.TxCreateDoc) { + const createTx = tx as TxCreateDoc + const { online } = createTx.attributes + if (online) { + const status = TxProcessor.createDoc2Doc(createTx) + await syncChat(control, status, originTx.modifiedOn) + } + } else if (tx._class === core.class.TxUpdateDoc) { + const updateTx = tx as TxUpdateDoc + const { online } = updateTx.operations + if (online === true) { + const status = (await control.findAll(control.ctx, core.class.UserStatus, { _id: updateTx.objectId }))[0] + await syncChat(control, status, originTx.modifiedOn) + } } } diff --git a/server-plugins/contact-resources/src/index.ts b/server-plugins/contact-resources/src/index.ts index f1235f9c75..311204d66c 100644 --- a/server-plugins/contact-resources/src/index.ts +++ b/server-plugins/contact-resources/src/index.ts @@ -50,63 +50,71 @@ import { getMetadata } from '@hcengineering/platform' import serverCore, { TriggerControl } from '@hcengineering/server-core' import { workbenchId } from '@hcengineering/workbench' -export async function OnSpaceTypeMembers (tx: Tx, control: TriggerControl): Promise { - const ctx = tx as TxUpdateDoc +export async function OnSpaceTypeMembers (txes: Tx[], control: TriggerControl): Promise { const result: Tx[] = [] - const newMember = ctx.operations.$push?.members as Ref - if (newMember !== undefined) { - const spaces = await control.findAll(control.ctx, core.class.Space, { type: ctx.objectId }) - for (const space of spaces) { - if (space.members.includes(newMember)) continue - const pushTx = control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, { - $push: { - members: newMember - } - }) - result.push(pushTx) + for (const tx of txes) { + const ctx = tx as TxUpdateDoc + const newMember = ctx.operations.$push?.members as Ref + if (newMember !== undefined) { + const spaces = await control.findAll(control.ctx, core.class.Space, { type: ctx.objectId }) + for (const space of spaces) { + if (space.members.includes(newMember)) continue + const pushTx = control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, { + $push: { + members: newMember + } + }) + result.push(pushTx) + } } - } - const oldMember = ctx.operations.$pull?.members as Ref - if (ctx.operations.$pull?.members !== undefined) { - const spaces = await control.findAll(control.ctx, core.class.Space, { type: ctx.objectId }) - for (const space of spaces) { - if (!space.members.includes(oldMember)) continue - const pullTx = control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, { - $pull: { - members: oldMember - } - }) - result.push(pullTx) + const oldMember = ctx.operations.$pull?.members as Ref + if (ctx.operations.$pull?.members !== undefined) { + const spaces = await control.findAll(control.ctx, core.class.Space, { type: ctx.objectId }) + for (const space of spaces) { + if (!space.members.includes(oldMember)) continue + const pullTx = control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, { + $pull: { + members: oldMember + } + }) + result.push(pullTx) + } } } return result } -export async function OnEmployeeCreate (tx: Tx, control: TriggerControl): Promise { - const mixinTx = tx as TxMixin - if (mixinTx.attributes.active !== true) return [] - const acc = control.modelDb.getAccountByPersonId(mixinTx.objectId) - if (acc.length === 0) return [] - const spaces = await control.findAll(control.ctx, core.class.Space, { autoJoin: true }) +export async function OnEmployeeCreate (_txes: Tx[], control: TriggerControl): Promise { const result: Tx[] = [] + for (const tx of _txes) { + const mixinTx = tx as TxMixin + if (mixinTx.attributes.active !== true) { + continue + } + const acc = control.modelDb.getAccountByPersonId(mixinTx.objectId) + if (acc.length === 0) { + continue + } + const spaces = await control.findAll(control.ctx, core.class.Space, { autoJoin: true }) - const txes = await createPersonSpace( - acc.map((it) => it._id), - mixinTx.objectId, - control - ) - result.push(...txes) + const txes = await createPersonSpace( + acc.map((it) => it._id), + mixinTx.objectId, + control + ) + result.push(...txes) - for (const space of spaces) { - const toAdd = acc.filter((it) => !space.members.includes(it._id)) - if (toAdd.length === 0) continue - for (const a of toAdd) { - const pushTx = control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, { - $push: { - members: a._id - } - }) - result.push(pushTx) + for (const space of spaces) { + const toAdd = acc.filter((it) => !space.members.includes(it._id)) + if (toAdd.length === 0) continue + for (const a of toAdd) { + const pushTx = control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, { + $push: { + members: a._id + } + }) + result.push(pushTx) + } } } return result @@ -142,32 +150,37 @@ async function createPersonSpace ( ] } -export async function OnPersonAccountCreate (tx: Tx, control: TriggerControl): Promise { - const acc = TxProcessor.createDoc2Doc(tx as TxCreateDoc) - const person = ( - await control.findAll( - control.ctx, - contact.mixin.Employee, - { _id: acc.person as Ref, active: true }, - { limit: 1 } - ) - )[0] - if (person === undefined) return [] +export async function OnPersonAccountCreate (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] const spaces = await control.findAll(control.ctx, core.class.Space, { autoJoin: true }) - const result: Tx[] = [] - const txes = await createPersonSpace([acc._id], person._id, control) + for (const tx of txes) { + const acc = TxProcessor.createDoc2Doc(tx as TxCreateDoc) + const person = ( + await control.findAll( + control.ctx, + contact.mixin.Employee, + { _id: acc.person as Ref, active: true }, + { limit: 1 } + ) + )[0] + if (person === undefined) { + continue + } - result.push(...txes) + const txes = await createPersonSpace([acc._id], person._id, control) - for (const space of spaces) { - if (space.members.includes(acc._id)) continue - const pushTx = control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, { - $push: { - members: acc._id - } - }) - result.push(pushTx) + result.push(...txes) + + for (const space of spaces) { + if (space.members.includes(acc._id)) continue + const pushTx = control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, { + $push: { + members: acc._id + } + }) + result.push(pushTx) + } } return result } @@ -176,29 +189,30 @@ export async function OnPersonAccountCreate (tx: Tx, control: TriggerControl): P * @public */ export async function OnContactDelete ( - tx: Tx, + txes: Tx[], { findAll, hierarchy, storageAdapter, workspace, removedMap, txFactory, ctx }: TriggerControl ): Promise { - const rmTx = tx as TxRemoveDoc - - const removeContact = removedMap.get(rmTx.objectId) as Contact - if (removeContact === undefined) { - return [] - } - const result: Tx[] = [] + for (const tx of txes) { + const rmTx = tx as TxRemoveDoc - const members = await findAll(ctx, contact.class.Member, { contact: removeContact._id }) - for (const member of members) { - const removeTx = txFactory.createTxRemoveDoc(member._class, member.space, member._id) - const tx = txFactory.createTxCollectionCUD( - member.attachedToClass, - member.attachedTo, - member.space, - member.collection, - removeTx - ) - result.push(tx) + const removeContact = removedMap.get(rmTx.objectId) as Contact + if (removeContact === undefined) { + continue + } + + const members = await findAll(ctx, contact.class.Member, { contact: removeContact._id }) + for (const member of members) { + const removeTx = txFactory.createTxRemoveDoc(member._class, member.space, member._id) + const tx = txFactory.createTxCollectionCUD( + member.attachedToClass, + member.attachedTo, + member.space, + member.collection, + removeTx + ) + result.push(tx) + } } return result @@ -207,36 +221,37 @@ export async function OnContactDelete ( /** * @public */ -export async function OnChannelUpdate (tx: Tx, control: TriggerControl): Promise { - const uTx = tx as TxUpdateDoc - +export async function OnChannelUpdate (txes: Tx[], control: TriggerControl): Promise { const result: Tx[] = [] + for (const tx of txes) { + const uTx = tx as TxUpdateDoc - if (uTx.operations.$inc?.items !== undefined) { - const doc = (await control.findAll(control.ctx, uTx.objectClass, { _id: uTx.objectId }, { limit: 1 }))[0] - if (doc !== undefined) { - if (control.hierarchy.hasMixin(doc, notification.mixin.Collaborators)) { - const collab = control.hierarchy.as(doc, notification.mixin.Collaborators) as Doc as Collaborators - if (collab.collaborators.includes(tx.modifiedBy)) { - result.push( - control.txFactory.createTxMixin(doc._id, doc._class, doc.space, notification.mixin.Collaborators, { - $push: { - collaborators: tx.modifiedBy - } - }) - ) - } - } else { - const res = control.txFactory.createTxMixin( - doc._id, - doc._class, - doc.space, - notification.mixin.Collaborators, - { - collaborators: [tx.modifiedBy] + if (uTx.operations.$inc?.items !== undefined) { + const doc = (await control.findAll(control.ctx, uTx.objectClass, { _id: uTx.objectId }, { limit: 1 }))[0] + if (doc !== undefined) { + if (control.hierarchy.hasMixin(doc, notification.mixin.Collaborators)) { + const collab = control.hierarchy.as(doc, notification.mixin.Collaborators) as Doc as Collaborators + if (collab.collaborators.includes(tx.modifiedBy)) { + result.push( + control.txFactory.createTxMixin(doc._id, doc._class, doc.space, notification.mixin.Collaborators, { + $push: { + collaborators: tx.modifiedBy + } + }) + ) } - ) - result.push(res) + } else { + const res = control.txFactory.createTxMixin( + doc._id, + doc._class, + doc.space, + notification.mixin.Collaborators, + { + collaborators: [tx.modifiedBy] + } + ) + result.push(res) + } } } } diff --git a/server-plugins/controlled-documents-resources/src/index.ts b/server-plugins/controlled-documents-resources/src/index.ts index abcb30241d..28cb724eff 100644 --- a/server-plugins/controlled-documents-resources/src/index.ts +++ b/server-plugins/controlled-documents-resources/src/index.ts @@ -1,22 +1,7 @@ // // Copyright © 2023-2024 Hardcore Engineering Inc. // -import core, { - DocumentQuery, - Ref, - SortingOrder, - Tx, - TxCollectionCUD, - TxFactory, - TxUpdateDoc, - type Timestamp, - type Account, - type RolesAssignment, - AccountRole, - TxCreateDoc -} from '@hcengineering/core' import contact, { type Employee, type PersonAccount } from '@hcengineering/contact' -import { TriggerControl } from '@hcengineering/server-core' import documents, { ControlledDocument, ControlledDocumentState, @@ -24,13 +9,28 @@ import documents, { DocumentApprovalRequest, DocumentState, DocumentTemplate, - type DocumentTraining, - getEffectiveDocUpdate, getDocumentId, - type DocumentRequest + getEffectiveDocUpdate, + type DocumentRequest, + type DocumentTraining } from '@hcengineering/controlled-documents' -import training, { type TrainingRequest, TrainingState } from '@hcengineering/training' +import core, { + AccountRole, + DocumentQuery, + Ref, + SortingOrder, + Tx, + TxCollectionCUD, + TxCreateDoc, + TxFactory, + TxUpdateDoc, + type Account, + type RolesAssignment, + type Timestamp +} from '@hcengineering/core' import { RequestStatus } from '@hcengineering/request' +import { TriggerControl } from '@hcengineering/server-core' +import training, { TrainingState, type TrainingRequest } from '@hcengineering/training' async function getDocs ( control: TriggerControl, @@ -272,130 +272,145 @@ function updateTemplate (doc: ControlledDocument, olderEffective: ControlledDocu } export async function OnDocHasBecomeEffective ( - tx: TxUpdateDoc, + txes: TxUpdateDoc[], control: TriggerControl ): Promise { - const doc = (await control.findAll(control.ctx, tx.objectClass, { _id: tx.objectId }, { limit: 1 })).shift() - if (doc === undefined) { - return [] + const result: Tx[] = [] + for (const tx of txes) { + const doc = (await control.findAll(control.ctx, tx.objectClass, { _id: tx.objectId }, { limit: 1 })).shift() + if (doc === undefined) { + continue + } + + const olderEffective = await getDocsOlderThanDoc(doc, control, [DocumentState.Effective]) + + result.push( + ...updateAuthor(doc, control.txFactory), + ...archiveDocs(olderEffective, control.txFactory), + ...updateMeta(doc, control.txFactory), + ...updateTemplate(doc, olderEffective, control), + ...(await createDocumentTrainingRequest(doc, control)) + ) } - - const olderEffective = await getDocsOlderThanDoc(doc, control, [DocumentState.Effective]) - - return [ - ...updateAuthor(doc, control.txFactory), - ...archiveDocs(olderEffective, control.txFactory), - ...updateMeta(doc, control.txFactory), - ...updateTemplate(doc, olderEffective, control), - ...(await createDocumentTrainingRequest(doc, control)) - ] + return result } -export async function OnDocDeleted (tx: TxUpdateDoc, control: TriggerControl): Promise { - const requests = await control.findAll(control.ctx, documents.class.DocumentRequest, { - attachedTo: tx.objectId, - status: RequestStatus.Active - }) - const cancelTxes = requests.map((request) => - control.txFactory.createTxUpdateDoc(request._class, request.space, request._id, { - status: RequestStatus.Cancelled +export async function OnDocDeleted (txes: TxUpdateDoc[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + const requests = await control.findAll(control.ctx, documents.class.DocumentRequest, { + attachedTo: tx.objectId, + status: RequestStatus.Active }) - ) - await control.apply(control.ctx, [ - ...cancelTxes, - control.txFactory.createTxUpdateDoc(tx.objectClass, tx.objectSpace, tx.objectId, { - controlledState: undefined - }) - ]) + const cancelTxes = requests.map((request) => + control.txFactory.createTxUpdateDoc(request._class, request.space, request._id, { + status: RequestStatus.Cancelled + }) + ) + await control.apply(control.ctx, [ + ...cancelTxes, + control.txFactory.createTxUpdateDoc(tx.objectClass, tx.objectSpace, tx.objectId, { + controlledState: undefined + }) + ]) + } - return [] + return result } export async function OnDocPlannedEffectiveDateChanged ( - tx: TxUpdateDoc, + txes: TxUpdateDoc[], control: TriggerControl ): Promise { - if (!('plannedEffectiveDate' in tx.operations)) { - return [] + const result: Tx[] = [] + for (const tx of txes) { + if (!('plannedEffectiveDate' in tx.operations)) { + continue + } + + const doc = (await control.findAll(control.ctx, tx.objectClass, { _id: tx.objectId }, { limit: 1 })).shift() + if (doc === undefined) { + continue + } + + // make doc effective immediately if required + if (tx.operations.plannedEffectiveDate === 0 && doc.controlledState === ControlledDocumentState.Approved) { + // Create with not derived tx factory in order for notifications to work + const factory = new TxFactory(control.txFactory.account) + await control.apply(control.ctx, [makeDocEffective(doc, factory)]) + } } - const doc = (await control.findAll(control.ctx, tx.objectClass, { _id: tx.objectId }, { limit: 1 })).shift() - if (doc === undefined) { - return [] - } - - // make doc effective immediately if required - if (tx.operations.plannedEffectiveDate === 0 && doc.controlledState === ControlledDocumentState.Approved) { - // Create with not derived tx factory in order for notifications to work - const factory = new TxFactory(control.txFactory.account) - await control.apply(control.ctx, [makeDocEffective(doc, factory)]) - } - - return [] + return result } export async function OnDocApprovalRequestApproved ( - tx: TxCollectionCUD, + txes: TxCollectionCUD[], control: TriggerControl ): Promise { - const doc = (await control.findAll(control.ctx, tx.objectClass, { _id: tx.objectId })).shift() - if (doc == null || doc.plannedEffectiveDate !== 0) { - return [] + const result: Tx[] = [] + for (const tx of txes) { + const doc = (await control.findAll(control.ctx, tx.objectClass, { _id: tx.objectId })).shift() + if (doc == null || doc.plannedEffectiveDate !== 0) { + continue + } + + // Create with not derived tx factory in order for notifications to work + const factory = new TxFactory(control.txFactory.account) + await control.apply(control.ctx, [makeDocEffective(doc, factory)]) + // make doc effective immediately } - - // Create with not derived tx factory in order for notifications to work - const factory = new TxFactory(control.txFactory.account) - await control.apply(control.ctx, [makeDocEffective(doc, factory)]) - - // make doc effective immediately - return [] + return result } /** * @public */ -export async function OnWorkspaceOwnerAdded (tx: Tx, control: TriggerControl): Promise { - let ownerId: Ref | undefined - if (control.hierarchy.isDerived(tx._class, core.class.TxCreateDoc)) { - const createTx = tx as TxCreateDoc +export async function OnWorkspaceOwnerAdded (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + let ownerId: Ref | undefined + if (control.hierarchy.isDerived(tx._class, core.class.TxCreateDoc)) { + const createTx = tx as TxCreateDoc - if (createTx.attributes.role === AccountRole.Owner) { - ownerId = createTx.objectId + if (createTx.attributes.role === AccountRole.Owner) { + ownerId = createTx.objectId + } + } else if (control.hierarchy.isDerived(tx._class, core.class.TxUpdateDoc)) { + const updateTx = tx as TxUpdateDoc + + if (updateTx.operations.role === AccountRole.Owner) { + ownerId = updateTx.objectId + } } - } else if (control.hierarchy.isDerived(tx._class, core.class.TxUpdateDoc)) { - const updateTx = tx as TxUpdateDoc - if (updateTx.operations.role === AccountRole.Owner) { - ownerId = updateTx.objectId + if (ownerId === undefined) { + continue + } + + const targetSpace = ( + await control.findAll(control.ctx, documents.class.OrgSpace, { + _id: documents.space.QualityDocuments + }) + )[0] + + if (targetSpace === undefined) { + continue + } + + if ( + targetSpace.owners === undefined || + targetSpace.owners.length === 0 || + targetSpace.owners[0] === core.account.System + ) { + const updTx = control.txFactory.createTxUpdateDoc(documents.class.OrgSpace, targetSpace.space, targetSpace._id, { + owners: [ownerId] + }) + result.push(updTx) } } - if (ownerId === undefined) { - return [] - } - - const targetSpace = ( - await control.findAll(control.ctx, documents.class.OrgSpace, { - _id: documents.space.QualityDocuments - }) - )[0] - - if (targetSpace === undefined) { - return [] - } - - if ( - targetSpace.owners === undefined || - targetSpace.owners.length === 0 || - targetSpace.owners[0] === core.account.System - ) { - const updTx = control.txFactory.createTxUpdateDoc(documents.class.OrgSpace, targetSpace.space, targetSpace._id, { - owners: [ownerId] - }) - return [updTx] - } - - return [] + return result } export async function documentTextPresenter (doc: ControlledDocument): Promise { diff --git a/server-plugins/drive-resources/src/index.ts b/server-plugins/drive-resources/src/index.ts index 8e0f6c996a..80464092a5 100644 --- a/server-plugins/drive-resources/src/index.ts +++ b/server-plugins/drive-resources/src/index.ts @@ -20,28 +20,35 @@ import { type Ref, type Tx, type TxRemoveDoc, - TxProcessor, DocumentQuery, FindOptions, - FindResult + FindResult, + TxProcessor } from '@hcengineering/core' import drive, { type FileVersion, type Folder } from '@hcengineering/drive' import type { TriggerControl } from '@hcengineering/server-core' /** @public */ export async function OnFileVersionDelete ( - tx: Tx, + txes: Tx[], { removedMap, ctx, storageAdapter, workspace }: TriggerControl ): Promise { - const rmTx = TxProcessor.extractTx(tx) as TxRemoveDoc + const result: Tx[] = [] + const toDelete: string[] = [] + for (const tx of txes) { + const rmTx = TxProcessor.extractTx(tx) as TxRemoveDoc - // Obtain document being deleted. - const version = removedMap.get(rmTx.objectId) as FileVersion - if (version !== undefined) { - await storageAdapter.remove(ctx, workspace, [version.file]) + // Obtain document being deleted. + const version = removedMap.get(rmTx.objectId) as FileVersion + if (version !== undefined) { + toDelete.push(version.file) + } + } + if (toDelete.length > 0) { + await storageAdapter.remove(ctx, workspace, toDelete) } - return [] + return result } /** @public */ diff --git a/server-plugins/fulltext-resources/src/index.ts b/server-plugins/fulltext-resources/src/index.ts index 14b4b83380..b57617843d 100644 --- a/server-plugins/fulltext-resources/src/index.ts +++ b/server-plugins/fulltext-resources/src/index.ts @@ -15,18 +15,19 @@ import core, { DocIndexState, - DOMAIN_DOC_INDEX_STATE, - getFullTextContext, isClassIndexable, + isFullTextAttribute, Tx, TxProcessor, + type AnyAttribute, type AttachedDoc, type Class, type Doc, type FullTextSearchContext, type Ref, type TxCollectionCUD, - type TxCUD + type TxCUD, + type TxUpdateDoc } from '@hcengineering/core' import { TriggerControl } from '@hcengineering/server-core' @@ -38,13 +39,17 @@ export async function OnChange (txes: Tx[], control: TriggerControl): Promise, DocIndexState>() + if (control.ctx.contextData.fulltextUpdates === undefined) { + control.ctx.contextData.fulltextUpdates = new Map() + } + const docsToUpsert = control.ctx.contextData.fulltextUpdates for (let tx of txes) { if (tx._class === core.class.TxCollectionCUD) { const txcol = tx as TxCollectionCUD tx = txcol.tx } + const attrs = new Map() if (TxProcessor.isExtendsCUD(tx._class)) { const cud = tx as TxCUD @@ -52,6 +57,35 @@ export async function OnChange (txes: Tx[], control: TriggerControl): Promise + for (const [k] of Object.entries(upd.operations)) { + if (k.startsWith('$') || k === 'modifiedOn') { + // Is operator + // Skip operator changes + } else { + const key = upd.objectClass + '.' + k + const attr = attrs.get(key) ?? control.hierarchy.getAttribute(upd.objectClass, k) + if (attr !== undefined) { + attrs.set(key, attr ?? null) + } + if (attr != null) { + if (isFullTextAttribute(attr)) { + hasFulltext = true + } + } + } + } + if (!hasFulltext) { + // No full text changes, no indexing is required + continue + } + } + docsToUpsert.set(cud.objectId as Ref, { _id: cud.objectId as Ref, _class: core.class.DocIndexState, @@ -62,25 +96,8 @@ export async function OnChange (txes: Tx[], control: TriggerControl): Promise 0) { - control.ctx.contextData.needWarmupFulltext = true - await control.ctx.with('clean-childs', { _class: cud.objectClass }, () => { - // We need to propagate all changes to all child's of following classes. - return control.lowLevel.rawUpdate( - DOMAIN_DOC_INDEX_STATE, - { attachedTo: cud.objectClass, objectClass: { $in: propagate } }, - { needIndex: true } - ) - }) - } } } - if (docsToUpsert.size > 0) { - control.ctx.contextData.needWarmupFulltext = true - await control.lowLevel.upload(control.ctx, DOMAIN_DOC_INDEX_STATE, Array.from(docsToUpsert.values())) - } return [] } diff --git a/server-plugins/gmail-resources/src/index.ts b/server-plugins/gmail-resources/src/index.ts index cc97e92b83..36774a299d 100644 --- a/server-plugins/gmail-resources/src/index.ts +++ b/server-plugins/gmail-resources/src/index.ts @@ -65,26 +65,27 @@ export async function FindMessages ( /** * @public */ -export async function OnMessageCreate (tx: Tx, control: TriggerControl): Promise { - const res: Tx[] = [] +export async function OnMessageCreate (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + const createTx = tx as TxCreateDoc - const createTx = tx as TxCreateDoc + const message = TxProcessor.createDoc2Doc(createTx) - const message = TxProcessor.createDoc2Doc(createTx) - - const channel = ( - await control.findAll(control.ctx, contact.class.Channel, { _id: message.attachedTo }, { limit: 1 }) - )[0] - if (channel !== undefined) { - if (channel.lastMessage === undefined || channel.lastMessage < message.sendOn) { - const tx = control.txFactory.createTxUpdateDoc(channel._class, channel.space, channel._id, { - lastMessage: message.sendOn - }) - res.push(tx) + const channel = ( + await control.findAll(control.ctx, contact.class.Channel, { _id: message.attachedTo }, { limit: 1 }) + )[0] + if (channel !== undefined) { + if (channel.lastMessage === undefined || channel.lastMessage < message.sendOn) { + const tx = control.txFactory.createTxUpdateDoc(channel._class, channel.space, channel._id, { + lastMessage: message.sendOn + }) + result.push(tx) + } } } - return res + return result } /** diff --git a/server-plugins/guest-resources/src/index.ts b/server-plugins/guest-resources/src/index.ts index 9b51c17952..dc17d073fb 100644 --- a/server-plugins/guest-resources/src/index.ts +++ b/server-plugins/guest-resources/src/index.ts @@ -35,24 +35,27 @@ import view from '@hcengineering/view' /** * @public */ -export async function OnPublicLinkCreate (tx: Tx, control: TriggerControl): Promise { - const res: Tx[] = [] +export async function OnPublicLinkCreate (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + const extractedTx = TxProcessor.extractTx(tx) - const extractedTx = TxProcessor.extractTx(tx) + const createTx = extractedTx as TxCreateDoc - const createTx = extractedTx as TxCreateDoc + const link = TxProcessor.createDoc2Doc(createTx) - const link = TxProcessor.createDoc2Doc(createTx) + if (link.url !== '') { + continue + } - if (link.url !== '') return res + const resTx = control.txFactory.createTxUpdateDoc(link._class, link.space, link._id, { + url: generateUrl(link._id, control.workspace, control.branding?.front) + }) - const resTx = control.txFactory.createTxUpdateDoc(link._class, link.space, link._id, { - url: generateUrl(link._id, control.workspace, control.branding?.front) - }) + result.push(resTx) + } - res.push(resTx) - - return res + return result } export function getPublicLinkUrl (workspace: WorkspaceIdWithUrl, brandedFront?: string): string { diff --git a/server-plugins/hr-resources/src/index.ts b/server-plugins/hr-resources/src/index.ts index 23da96c165..a7577a049f 100644 --- a/server-plugins/hr-resources/src/index.ts +++ b/server-plugins/hr-resources/src/index.ts @@ -131,135 +131,164 @@ function getTxes ( /** * @public */ -export async function OnDepartmentStaff (tx: Tx, control: TriggerControl): Promise { - const ctx = TxProcessor.extractTx(tx) as TxMixin +export async function OnDepartmentStaff (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + const ctx = TxProcessor.extractTx(tx) as TxMixin - const targetAccount = control.modelDb.getAccountByPersonId(ctx.objectId) as PersonAccount[] - if (targetAccount.length === 0) return [] + const targetAccount = control.modelDb.getAccountByPersonId(ctx.objectId) as PersonAccount[] + if (targetAccount.length === 0) { + continue + } - if (ctx.attributes.department !== undefined) { - const lastDepartment = await getOldDepartment(ctx, control) + if (ctx.attributes.department !== undefined) { + const lastDepartment = await getOldDepartment(ctx, control) - const departmentId = ctx.attributes.department - if (departmentId === null) { - if (lastDepartment !== undefined) { - const removed = await buildHierarchy(lastDepartment, control) - return getTxes( - control.txFactory, - targetAccount.map((it) => it._id), - [], - removed.map((p) => p._id) + const departmentId = ctx.attributes.department + if (departmentId === null) { + if (lastDepartment !== undefined) { + const removed = await buildHierarchy(lastDepartment, control) + result.push( + ...getTxes( + control.txFactory, + targetAccount.map((it) => it._id), + [], + removed.map((p) => p._id) + ) + ) + } + } + const push = (await buildHierarchy(departmentId, control)).map((p) => p._id) + + if (lastDepartment === undefined) { + result.push( + ...getTxes( + control.txFactory, + targetAccount.map((it) => it._id), + push + ) + ) + } else { + let removed = (await buildHierarchy(lastDepartment, control)).map((p) => p._id) + const added = exlude(removed, push) + removed = exlude(push, removed) + result.push( + ...getTxes( + control.txFactory, + targetAccount.map((it) => it._id), + added, + removed + ) ) } } - const push = (await buildHierarchy(departmentId, control)).map((p) => p._id) + } - if (lastDepartment === undefined) { - return getTxes( - control.txFactory, - targetAccount.map((it) => it._id), - push + return result +} + +/** + * @public + */ +export async function OnDepartmentRemove (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + const ctx = TxProcessor.extractTx(tx) as TxRemoveDoc + + const department = control.removedMap.get(ctx.objectId) as Department + if (department === undefined) { + continue + } + const nested = await control.findAll(control.ctx, hr.class.Department, { parent: department._id }) + for (const dep of nested) { + result.push(control.txFactory.createTxRemoveDoc(dep._class, dep.space, dep._id)) + } + const targetAccounts = await control.modelDb.findAll(contact.class.PersonAccount, { + _id: { $in: department.members } + }) + const employeeIds = targetAccounts.map((acc) => acc.person as Ref) + + const employee = await control.findAll(control.ctx, contact.mixin.Employee, { + _id: { $in: employeeIds } + }) + const removed = await buildHierarchy(department._id, control) + employee.forEach((em) => { + result.push( + control.txFactory.createTxMixin(em._id, em._class, em.space, hr.mixin.Staff, { department: undefined }) ) + }) + result.push( + ...getTxes( + control.txFactory, + targetAccounts.map((it) => it._id), + [], + removed.map((p) => p._id) + ) + ) + } + return result +} + +/** + * @public + */ +export async function OnEmployee (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + const ctx = TxProcessor.extractTx(tx) as TxMixin + + const person = (await control.findAll(control.ctx, contact.class.Person, { _id: ctx.objectId }))[0] + if (person === undefined) { + continue } - let removed = (await buildHierarchy(lastDepartment, control)).map((p) => p._id) - const added = exlude(removed, push) - removed = exlude(push, removed) - return getTxes( - control.txFactory, - targetAccount.map((it) => it._id), - added, - removed + const employee = control.hierarchy.as(person, ctx.mixin) + if (control.hierarchy.hasMixin(person, hr.mixin.Staff) || !employee.active) { + continue + } + + result.push( + control.txFactory.createTxMixin(ctx.objectId, ctx.objectClass, ctx.objectSpace, hr.mixin.Staff, { + department: hr.ids.Head + }) ) } - - return [] + return result } /** * @public */ -export async function OnDepartmentRemove (tx: Tx, control: TriggerControl): Promise { - const ctx = TxProcessor.extractTx(tx) as TxRemoveDoc +export async function OnEmployeeDeactivate (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + const actualTx = TxProcessor.extractTx(tx) + if (core.class.TxMixin !== actualTx._class) { + continue + } + const ctx = actualTx as TxMixin + if (ctx.mixin !== contact.mixin.Employee || ctx.attributes.active !== false) { + continue + } - const department = control.removedMap.get(ctx.objectId) as Department - if (department === undefined) return [] - const res: Tx[] = [] - const nested = await control.findAll(control.ctx, hr.class.Department, { parent: department._id }) - for (const dep of nested) { - res.push(control.txFactory.createTxRemoveDoc(dep._class, dep.space, dep._id)) - } - const targetAccounts = await control.modelDb.findAll(contact.class.PersonAccount, { - _id: { $in: department.members } - }) - const employeeIds = targetAccounts.map((acc) => acc.person as Ref) + const targetAccount = control.modelDb.getAccountByPersonId(ctx.objectId) as PersonAccount[] + if (targetAccount.length === 0) { + continue + } + const set = new Set(targetAccount.map((it) => it._id)) - const employee = await control.findAll(control.ctx, contact.mixin.Employee, { - _id: { $in: employeeIds } - }) - const removed = await buildHierarchy(department._id, control) - employee.forEach((em) => { - res.push(control.txFactory.createTxMixin(em._id, em._class, em.space, hr.mixin.Staff, { department: undefined })) - }) - res.push( - ...getTxes( - control.txFactory, - targetAccounts.map((it) => it._id), - [], - removed.map((p) => p._id) + const departments = await control.queryFind(control.ctx, hr.class.Department, {}) + const removed = departments.filter((dep) => dep.members.some((p) => set.has(p))) + result.push( + ...getTxes( + control.txFactory, + targetAccount.map((it) => it._id), + [], + removed.map((p) => p._id) + ) ) - ) - return res -} - -/** - * @public - */ -export async function OnEmployee (tx: Tx, control: TriggerControl): Promise { - const ctx = TxProcessor.extractTx(tx) as TxMixin - - const person = (await control.findAll(control.ctx, contact.class.Person, { _id: ctx.objectId }))[0] - if (person === undefined) { - return [] } - - const employee = control.hierarchy.as(person, ctx.mixin) - if (control.hierarchy.hasMixin(person, hr.mixin.Staff) || !employee.active) { - return [] - } - - return [ - control.txFactory.createTxMixin(ctx.objectId, ctx.objectClass, ctx.objectSpace, hr.mixin.Staff, { - department: hr.ids.Head - }) - ] -} - -/** - * @public - */ -export async function OnEmployeeDeactivate (tx: Tx, control: TriggerControl): Promise { - const actualTx = TxProcessor.extractTx(tx) - if (core.class.TxMixin !== actualTx._class) { - return [] - } - const ctx = actualTx as TxMixin - if (ctx.mixin !== contact.mixin.Employee || ctx.attributes.active !== false) { - return [] - } - - const targetAccount = control.modelDb.getAccountByPersonId(ctx.objectId) as PersonAccount[] - if (targetAccount.length === 0) return [] - const set = new Set(targetAccount.map((it) => it._id)) - - const departments = await control.queryFind(control.ctx, hr.class.Department, {}) - const removed = departments.filter((dep) => dep.members.some((p) => set.has(p))) - return getTxes( - control.txFactory, - targetAccount.map((it) => it._id), - [], - removed.map((p) => p._id) - ) + return result } // TODO: why we need specific email notifications instead of using general flow? @@ -317,47 +346,63 @@ async function sendEmailNotifications ( /** * @public */ -export async function OnRequestCreate (tx: Tx, control: TriggerControl): Promise { - const ctx = TxProcessor.extractTx(tx) as TxCreateDoc +export async function OnRequestCreate (txes: Tx[], control: TriggerControl): Promise { + for (const tx of txes) { + const ctx = TxProcessor.extractTx(tx) as TxCreateDoc - const sender = getPersonAccountById(ctx.modifiedBy, control) - if (sender === undefined) return [] + const sender = getPersonAccountById(ctx.modifiedBy, control) + if (sender === undefined) { + continue + } - const request = TxProcessor.createDoc2Doc(ctx) + const request = TxProcessor.createDoc2Doc(ctx) - await sendEmailNotifications(control, sender, request, request.department, hr.ids.CreateRequestNotification) + await sendEmailNotifications(control, sender, request, request.department, hr.ids.CreateRequestNotification) + } return [] } /** * @public */ -export async function OnRequestUpdate (tx: Tx, control: TriggerControl): Promise { - const ctx = TxProcessor.extractTx(tx) as TxUpdateDoc +export async function OnRequestUpdate (txes: Tx[], control: TriggerControl): Promise { + for (const tx of txes) { + const ctx = TxProcessor.extractTx(tx) as TxUpdateDoc - const sender = getPersonAccountById(ctx.modifiedBy, control) - if (sender === undefined) return [] + const sender = getPersonAccountById(ctx.modifiedBy, control) + if (sender === undefined) { + continue + } - const request = (await control.findAll(control.ctx, hr.class.Request, { _id: ctx.objectId }))[0] as Request - if (request === undefined) return [] + const request = (await control.findAll(control.ctx, hr.class.Request, { _id: ctx.objectId }))[0] as Request + if (request === undefined) { + continue + } - await sendEmailNotifications(control, sender, request, request.department, hr.ids.UpdateRequestNotification) + await sendEmailNotifications(control, sender, request, request.department, hr.ids.UpdateRequestNotification) + } return [] } /** * @public */ -export async function OnRequestRemove (tx: Tx, control: TriggerControl): Promise { - const ctx = TxProcessor.extractTx(tx) as TxCreateDoc +export async function OnRequestRemove (txes: Tx[], control: TriggerControl): Promise { + for (const tx of txes) { + const ctx = TxProcessor.extractTx(tx) as TxCreateDoc - const sender = getPersonAccountById(ctx.modifiedBy, control) - if (sender === undefined) return [] + const sender = getPersonAccountById(ctx.modifiedBy, control) + if (sender === undefined) { + continue + } - const request = control.removedMap.get(ctx.objectId) as Request - if (request === undefined) return [] + const request = control.removedMap.get(ctx.objectId) as Request + if (request === undefined) { + continue + } - await sendEmailNotifications(control, sender, request, request.department, hr.ids.RemoveRequestNotification) + await sendEmailNotifications(control, sender, request, request.department, hr.ids.RemoveRequestNotification) + } return [] } @@ -400,23 +445,30 @@ export async function RequestTextPresenter (doc: Doc, control: TriggerControl): /** * @public */ -export async function OnPublicHolidayCreate (tx: Tx, control: TriggerControl): Promise { - const ctx = TxProcessor.extractTx(tx) as TxCreateDoc +export async function OnPublicHolidayCreate (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + const ctx = TxProcessor.extractTx(tx) as TxCreateDoc - const sender = getPersonAccountById(ctx.modifiedBy, control) - if (sender === undefined) return [] - const employee = await getEmployee(sender.person as Ref, control) - if (employee === undefined) return [] + const sender = getPersonAccountById(ctx.modifiedBy, control) + if (sender === undefined) { + continue + } + const employee = await getEmployee(sender.person as Ref, control) + if (employee === undefined) { + continue + } - const publicHoliday = TxProcessor.createDoc2Doc(ctx) - await sendEmailNotifications( - control, - sender, - publicHoliday, - publicHoliday.department, - hr.ids.CreatePublicHolidayNotification - ) - return [] + const publicHoliday = TxProcessor.createDoc2Doc(ctx) + await sendEmailNotifications( + control, + sender, + publicHoliday, + publicHoliday.department, + hr.ids.CreatePublicHolidayNotification + ) + } + return result } /** diff --git a/server-plugins/lead-resources/src/index.ts b/server-plugins/lead-resources/src/index.ts index 84ec7326cd..a822163eb9 100644 --- a/server-plugins/lead-resources/src/index.ts +++ b/server-plugins/lead-resources/src/index.ts @@ -43,48 +43,51 @@ export async function leadTextPresenter (doc: Doc): Promise { /** * @public */ -export async function OnWorkspaceOwnerAdded (tx: Tx, control: TriggerControl): Promise { - let ownerId: Ref | undefined - if (control.hierarchy.isDerived(tx._class, core.class.TxCreateDoc)) { - const createTx = tx as TxCreateDoc +export async function OnWorkspaceOwnerAdded (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + let ownerId: Ref | undefined + if (control.hierarchy.isDerived(tx._class, core.class.TxCreateDoc)) { + const createTx = tx as TxCreateDoc - if (createTx.attributes.role === AccountRole.Owner) { - ownerId = createTx.objectId + if (createTx.attributes.role === AccountRole.Owner) { + ownerId = createTx.objectId + } + } else if (control.hierarchy.isDerived(tx._class, core.class.TxUpdateDoc)) { + const updateTx = tx as TxUpdateDoc + + if (updateTx.operations.role === AccountRole.Owner) { + ownerId = updateTx.objectId + } } - } else if (control.hierarchy.isDerived(tx._class, core.class.TxUpdateDoc)) { - const updateTx = tx as TxUpdateDoc - if (updateTx.operations.role === AccountRole.Owner) { - ownerId = updateTx.objectId + if (ownerId === undefined) { + continue + } + + const targetFunnel = ( + await control.findAll(control.ctx, lead.class.Funnel, { + _id: lead.space.DefaultFunnel + }) + )[0] + + if (targetFunnel === undefined) { + continue + } + + if ( + targetFunnel.owners === undefined || + targetFunnel.owners.length === 0 || + targetFunnel.owners[0] === core.account.System + ) { + const updTx = control.txFactory.createTxUpdateDoc(lead.class.Funnel, targetFunnel.space, targetFunnel._id, { + owners: [ownerId] + }) + result.push(updTx) } } - if (ownerId === undefined) { - return [] - } - - const targetFunnel = ( - await control.findAll(control.ctx, lead.class.Funnel, { - _id: lead.space.DefaultFunnel - }) - )[0] - - if (targetFunnel === undefined) { - return [] - } - - if ( - targetFunnel.owners === undefined || - targetFunnel.owners.length === 0 || - targetFunnel.owners[0] === core.account.System - ) { - const updTx = control.txFactory.createTxUpdateDoc(lead.class.Funnel, targetFunnel.space, targetFunnel._id, { - owners: [ownerId] - }) - return [updTx] - } - - return [] + return result } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type diff --git a/server-plugins/love-resources/src/index.ts b/server-plugins/love-resources/src/index.ts index c575789107..37cca9ba13 100644 --- a/server-plugins/love-resources/src/index.ts +++ b/server-plugins/love-resources/src/index.ts @@ -44,32 +44,41 @@ import { } from '@hcengineering/server-notification-resources' import { workbenchId } from '@hcengineering/workbench' -export async function OnEmployee (tx: Tx, control: TriggerControl): Promise { - const actualTx = TxProcessor.extractTx(tx) as TxMixin - if (actualTx._class !== core.class.TxMixin) return [] - if (actualTx.mixin !== contact.mixin.Employee) return [] - const val = actualTx.attributes.active - if (val === undefined) return [] - if (val) { - const freeRoom = (await control.findAll(control.ctx, love.class.Office, { person: null }))[0] - if (freeRoom !== undefined) { - return [ - control.txFactory.createTxUpdateDoc(freeRoom._class, freeRoom.space, freeRoom._id, { - person: actualTx.objectId - }) - ] +export async function OnEmployee (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + const actualTx = TxProcessor.extractTx(tx) as TxMixin + if (actualTx._class !== core.class.TxMixin) { + continue } - } else { - const room = (await control.findAll(control.ctx, love.class.Office, { person: actualTx.objectId }))[0] - if (room !== undefined) { - return [ - control.txFactory.createTxUpdateDoc(room._class, room.space, room._id, { - person: null - }) - ] + if (actualTx.mixin !== contact.mixin.Employee) { + continue + } + const val = actualTx.attributes.active + if (val === undefined) { + continue + } + if (val) { + const freeRoom = (await control.findAll(control.ctx, love.class.Office, { person: null }))[0] + if (freeRoom !== undefined) { + return [ + control.txFactory.createTxUpdateDoc(freeRoom._class, freeRoom.space, freeRoom._id, { + person: actualTx.objectId + }) + ] + } + } else { + const room = (await control.findAll(control.ctx, love.class.Office, { person: actualTx.objectId }))[0] + if (room !== undefined) { + result.push( + control.txFactory.createTxUpdateDoc(room._class, room.space, room._id, { + person: null + }) + ) + } } } - return [] + return result } async function createUserInfo (acc: Ref, control: TriggerControl): Promise { @@ -124,33 +133,32 @@ async function removeUserInfo (acc: Ref, control: TriggerControl): Prom return res } -export async function OnUserStatus (tx: Tx, control: TriggerControl): Promise { - const actualTx = TxProcessor.extractTx(tx) as TxCUD - if (actualTx.objectClass !== core.class.UserStatus) return [] - if (actualTx._class === core.class.TxCreateDoc) { - const createTx = actualTx as TxCreateDoc - const status = TxProcessor.createDoc2Doc(createTx) - return await createUserInfo(status.user, control) - } else if (actualTx._class === core.class.TxUpdateDoc) { - const updateTx = actualTx as TxUpdateDoc - const val = updateTx.operations.online - if (val === undefined) return [] - const status = (await control.findAll(control.ctx, core.class.UserStatus, { _id: updateTx.objectId }))[0] - if (status !== undefined) { - if (val) { - return await createUserInfo(status.user, control) - } else { - return await new Promise((resolve) => { - setTimeout(() => { - void removeUserInfo(status.user, control).then((res) => { - resolve(res) - }) - }, 20000) - }) +export async function OnUserStatus (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + const actualTx = TxProcessor.extractTx(tx) as TxCUD + if (actualTx.objectClass !== core.class.UserStatus) { + continue + } + if (actualTx._class === core.class.TxCreateDoc) { + const createTx = actualTx as TxCreateDoc + const status = TxProcessor.createDoc2Doc(createTx) + result.push(...(await createUserInfo(status.user, control))) + } else if (actualTx._class === core.class.TxUpdateDoc) { + const updateTx = actualTx as TxUpdateDoc + const val = updateTx.operations.online + if (val === undefined) return [] + const status = (await control.findAll(control.ctx, core.class.UserStatus, { _id: updateTx.objectId }))[0] + if (status !== undefined) { + if (val) { + result.push(...(await createUserInfo(status.user, control))) + } else { + result.push(...(await removeUserInfo(status.user, control))) + } } } } - return [] + return result } async function roomJoinHandler (info: ParticipantInfo, control: TriggerControl): Promise { @@ -228,110 +236,134 @@ async function setDefaultRoomAccess (info: ParticipantInfo, control: TriggerCont return res } -export async function OnParticipantInfo (tx: Tx, control: TriggerControl): Promise { - const actualTx = TxProcessor.extractTx(tx) as TxCUD - if (actualTx._class === core.class.TxCreateDoc) { - const info = TxProcessor.createDoc2Doc(actualTx as TxCreateDoc) - return await roomJoinHandler(info, control) +export async function OnParticipantInfo (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + const actualTx = TxProcessor.extractTx(tx) as TxCUD + if (actualTx._class === core.class.TxCreateDoc) { + const info = TxProcessor.createDoc2Doc(actualTx as TxCreateDoc) + result.push(...(await roomJoinHandler(info, control))) + } + if (actualTx._class === core.class.TxRemoveDoc) { + const removedInfo = control.removedMap.get(actualTx.objectId) as ParticipantInfo + if (removedInfo === undefined) { + continue + } + result.push(...(await setDefaultRoomAccess(removedInfo, control))) + continue + } + if (actualTx._class === core.class.TxUpdateDoc) { + const newRoom = (actualTx as TxUpdateDoc).operations.room + if (newRoom === undefined) { + continue + } + const info = ( + await control.findAll(control.ctx, love.class.ParticipantInfo, { _id: actualTx.objectId }, { limit: 1 }) + )[0] + if (info === undefined) { + continue + } + result.push(...(await rejectJoinRequests(info, control))) + result.push(...(await setDefaultRoomAccess(info, control))) + result.push(...(await roomJoinHandler(info, control))) + } } - if (actualTx._class === core.class.TxRemoveDoc) { - const removedInfo = control.removedMap.get(actualTx.objectId) as ParticipantInfo - if (removedInfo === undefined) return [] - return await setDefaultRoomAccess(removedInfo, control) - } - if (actualTx._class === core.class.TxUpdateDoc) { - const newRoom = (actualTx as TxUpdateDoc).operations.room - if (newRoom === undefined) return [] - const info = ( - await control.findAll(control.ctx, love.class.ParticipantInfo, { _id: actualTx.objectId }, { limit: 1 }) - )[0] - if (info === undefined) return [] - const res: Tx[] = [] - res.push(...(await rejectJoinRequests(info, control))) - res.push(...(await setDefaultRoomAccess(info, control))) - res.push(...(await roomJoinHandler(info, control))) - return res - } - return [] + return result } -export async function OnKnock (tx: Tx, control: TriggerControl): Promise { - const actualTx = TxProcessor.extractTx(tx) as TxCreateDoc - if (actualTx._class === core.class.TxCreateDoc) { - const request = TxProcessor.createDoc2Doc(actualTx) - if (request.status === RequestStatus.Pending) { - const roomInfo = (await control.findAll(control.ctx, love.class.RoomInfo, { room: request.room }))[0] - if (roomInfo !== undefined) { - const res: Tx[] = [] - const from = (await control.findAll(control.ctx, contact.class.Person, { _id: request.person }))[0] - if (from === undefined) return [] - const type = await control.modelDb.findOne(notification.class.NotificationType, { - _id: love.ids.KnockNotification - }) - if (type === undefined) return [] - const provider = await control.modelDb.findOne(notification.class.NotificationProvider, { - _id: notification.providers.PushNotificationProvider - }) - if (provider === undefined) return [] +export async function OnKnock (txes: Tx[], control: TriggerControl): Promise { + for (const tx of txes) { + const actualTx = TxProcessor.extractTx(tx) as TxCreateDoc + if (actualTx._class === core.class.TxCreateDoc) { + const request = TxProcessor.createDoc2Doc(actualTx) + if (request.status === RequestStatus.Pending) { + const roomInfo = (await control.findAll(control.ctx, love.class.RoomInfo, { room: request.room }))[0] + if (roomInfo !== undefined) { + const from = (await control.findAll(control.ctx, contact.class.Person, { _id: request.person }))[0] + if (from === undefined) { + continue + } + const type = await control.modelDb.findOne(notification.class.NotificationType, { + _id: love.ids.KnockNotification + }) + if (type === undefined) { + continue + } + const provider = await control.modelDb.findOne(notification.class.NotificationProvider, { + _id: notification.providers.PushNotificationProvider + }) + if (provider === undefined) { + continue + } - const notificationControl = await getNotificationProviderControl(control.ctx, control) - for (const user of roomInfo.persons) { - const userAcc = control.modelDb.getAccountByPersonId(user) as PersonAccount[] - if (userAcc.length === 0) continue - if (userAcc.some((it) => isAllowed(control, it._id, type, provider, notificationControl))) { - const path = [workbenchId, control.workspace.workspaceUrl, loveId] - const title = await translate(love.string.KnockingLabel, {}) - const body = await translate(love.string.IsKnocking, { - name: formatName(from.name, control.branding?.lastNameFirst) - }) + const notificationControl = await getNotificationProviderControl(control.ctx, control) + for (const user of roomInfo.persons) { + const userAcc = control.modelDb.getAccountByPersonId(user) as PersonAccount[] + if (userAcc.length === 0) continue + if (userAcc.some((it) => isAllowed(control, it._id, type, provider, notificationControl))) { + const path = [workbenchId, control.workspace.workspaceUrl, loveId] + const title = await translate(love.string.KnockingLabel, {}) + const body = await translate(love.string.IsKnocking, { + name: formatName(from.name, control.branding?.lastNameFirst) + }) - const subscriptions = await control.findAll(control.ctx, notification.class.PushSubscription, { - user: userAcc[0]._id - }) - // TODO: Select proper account target - await createPushNotification(control, userAcc[0]._id, title, body, request._id, subscriptions, from, path) + const subscriptions = await control.findAll(control.ctx, notification.class.PushSubscription, { + user: userAcc[0]._id + }) + // TODO: Select proper account target + await createPushNotification(control, userAcc[0]._id, title, body, request._id, subscriptions, from, path) + } } } - return res } } } return [] } -export async function OnInvite (tx: Tx, control: TriggerControl): Promise { - const actualTx = TxProcessor.extractTx(tx) as TxCreateDoc - if (actualTx._class === core.class.TxCreateDoc) { - const invite = TxProcessor.createDoc2Doc(actualTx) - if (invite.status === RequestStatus.Pending) { - const target = (await control.findAll(control.ctx, contact.class.Person, { _id: invite.target }))[0] - if (target === undefined) return [] - const userAcc = control.modelDb.getAccountByPersonId(target._id) as PersonAccount[] - if (userAcc.length === 0) return [] - const from = (await control.findAll(control.ctx, contact.class.Person, { _id: invite.from }))[0] - const type = await control.modelDb.findOne(notification.class.NotificationType, { - _id: love.ids.InviteNotification - }) - if (type === undefined) return [] - const provider = await control.modelDb.findOne(notification.class.NotificationProvider, { - _id: notification.providers.PushNotificationProvider - }) - if (provider === undefined) return [] - const notificationControl = await getNotificationProviderControl(control.ctx, control) - if (userAcc.some((it) => isAllowed(control, it._id, type, provider, notificationControl))) { - const path = [workbenchId, control.workspace.workspaceUrl, loveId] - const title = await translate(love.string.InivitingLabel, {}) - const body = - from !== undefined - ? await translate(love.string.InvitingYou, { - name: formatName(from.name, control.branding?.lastNameFirst) - }) - : await translate(love.string.InivitingLabel, {}) - const subscriptions = await control.findAll(control.ctx, notification.class.PushSubscription, { - user: userAcc[0]._id +export async function OnInvite (txes: Tx[], control: TriggerControl): Promise { + for (const tx of txes) { + const actualTx = TxProcessor.extractTx(tx) as TxCreateDoc + if (actualTx._class === core.class.TxCreateDoc) { + const invite = TxProcessor.createDoc2Doc(actualTx) + if (invite.status === RequestStatus.Pending) { + const target = (await control.findAll(control.ctx, contact.class.Person, { _id: invite.target }))[0] + if (target === undefined) { + continue + } + const userAcc = control.modelDb.getAccountByPersonId(target._id) as PersonAccount[] + if (userAcc.length === 0) { + continue + } + const from = (await control.findAll(control.ctx, contact.class.Person, { _id: invite.from }))[0] + const type = await control.modelDb.findOne(notification.class.NotificationType, { + _id: love.ids.InviteNotification }) - // TODO: Select a proper user - await createPushNotification(control, userAcc[0]._id, title, body, invite._id, subscriptions, from, path) + if (type === undefined) { + continue + } + const provider = await control.modelDb.findOne(notification.class.NotificationProvider, { + _id: notification.providers.PushNotificationProvider + }) + if (provider === undefined) { + continue + } + const notificationControl = await getNotificationProviderControl(control.ctx, control) + if (userAcc.some((it) => isAllowed(control, it._id, type, provider, notificationControl))) { + const path = [workbenchId, control.workspace.workspaceUrl, loveId] + const title = await translate(love.string.InivitingLabel, {}) + const body = + from !== undefined + ? await translate(love.string.InvitingYou, { + name: formatName(from.name, control.branding?.lastNameFirst) + }) + : await translate(love.string.InivitingLabel, {}) + const subscriptions = await control.findAll(control.ctx, notification.class.PushSubscription, { + user: userAcc[0]._id + }) + // TODO: Select a proper user + await createPushNotification(control, userAcc[0]._id, title, body, invite._id, subscriptions, from, path) + } } } } diff --git a/server-plugins/notification-resources/src/index.ts b/server-plugins/notification-resources/src/index.ts index 8910b19f37..16d7a8d97a 100644 --- a/server-plugins/notification-resources/src/index.ts +++ b/server-plugins/notification-resources/src/index.ts @@ -331,7 +331,7 @@ export async function getDocCollaborators ( const collaborators = new Set>() for (const field of mixin.fields) { const value = (doc as any)[field] - const newCollaborators = await ctx.with('getKeyCollaborators', {}, async (ctx) => + const newCollaborators = ctx.withSync('getKeyCollaborators', {}, (ctx) => getKeyCollaborators(doc._class, value, field, control) ) if (newCollaborators !== undefined) { @@ -991,10 +991,8 @@ export async function createCollabDocInfo ( return res } - const usersInfo = await ctx.with( - 'get-user-info', - {}, - async (ctx) => await getUsersInfo(ctx, [...Array.from(targets), originTx.modifiedBy as Ref], control) + const usersInfo = await ctx.with('get-user-info', {}, (ctx) => + getUsersInfo(ctx, [...Array.from(targets), originTx.modifiedBy as Ref], control) ) const sender: SenderInfo = usersInfo.get(originTx.modifiedBy) ?? { _id: originTx.modifiedBy @@ -1159,36 +1157,22 @@ async function createCollaboratorDoc ( } const doc = TxProcessor.createDoc2Doc(tx) - const collaborators = await ctx.with( - 'get-collaborators', - {}, - async (ctx) => await getDocCollaborators(ctx, doc, mixin, control) - ) + const collaborators = await ctx.with('get-collaborators', {}, (ctx) => getDocCollaborators(ctx, doc, mixin, control)) const mixinTx = getMixinTx(tx, control, collaborators) - const notificationTxes = await ctx.with( - 'create-collabdocinfo', - {}, - async (ctx) => - await createCollabDocInfo( - ctx, - collaborators as Ref[], - control, - tx, - originTx, - doc, - activityMessage, - { isOwn: true, isSpace: false, shouldUpdateTimestamp: true } - ) + const notificationTxes = await ctx.with('create-collabdocinfo', {}, (ctx) => + createCollabDocInfo(ctx, collaborators as Ref[], control, tx, originTx, doc, activityMessage, { + isOwn: true, + isSpace: false, + shouldUpdateTimestamp: true + }) ) res.push(mixinTx) res.push(...notificationTxes) res.push( - ...(await ctx.with( - 'get-space-collabtxes', - {}, - async (ctx) => await getSpaceCollabTxes(ctx, control, doc, tx, originTx, activityMessage, cache) + ...(await ctx.with('get-space-collabtxes', {}, (ctx) => + getSpaceCollabTxes(ctx, control, doc, tx, originTx, activityMessage, cache) )) ) @@ -1268,10 +1252,8 @@ async function updateCollaboratorsMixin ( objectId: tx.objectId }) - const infos = await ctx.with( - 'get-user-info', - {}, - async (ctx) => await getUsersInfo(ctx, [...newCollabs, originTx.modifiedBy] as Ref[], control) + const infos = await ctx.with('get-user-info', {}, (ctx) => + getUsersInfo(ctx, [...newCollabs, originTx.modifiedBy] as Ref[], control) ) const sender: SenderInfo = infos.get(originTx.modifiedBy) ?? { _id: originTx.modifiedBy } @@ -1332,27 +1314,15 @@ async function collectionCollabDoc ( cache.set(doc._id, doc) - const collaborators = await ctx.with( - 'get-collaborators', - {}, - async (ctx) => await getCollaborators(ctx, doc, control, tx, res) - ) + const collaborators = await ctx.with('get-collaborators', {}, (ctx) => getCollaborators(ctx, doc, control, tx, res)) res = res.concat( - await ctx.with( - 'create-collab-doc-info', - {}, - async (ctx) => - await createCollabDocInfo( - ctx, - collaborators as Ref[], - control, - actualTx, - tx, - doc, - activityMessages, - { isOwn: false, isSpace: false, shouldUpdateTimestamp: true } - ) + await ctx.with('create-collab-doc-info', {}, (ctx) => + createCollabDocInfo(ctx, collaborators as Ref[], control, actualTx, tx, doc, activityMessages, { + isOwn: false, + isSpace: false, + shouldUpdateTimestamp: true + }) ) ) @@ -1504,11 +1474,7 @@ async function updateCollaboratorDoc ( const params: NotifyParams = { isOwn: true, isSpace: false, shouldUpdateTimestamp: true } if (hierarchy.hasMixin(doc, notification.mixin.Collaborators)) { // we should handle change field and subscribe new collaborators - const collabsInfo = await ctx.with( - 'get-tx-collaborators', - {}, - async (ctx) => await getTxCollabs(ctx, tx, control, doc) - ) + const collabsInfo = await ctx.with('get-tx-collaborators', {}, (ctx) => getTxCollabs(ctx, tx, control, doc)) if (collabsInfo.added.length > 0) { res.push(createPushCollaboratorsTx(control, tx.objectId, tx.objectClass, tx.objectSpace, collabsInfo.added)) @@ -1519,28 +1485,23 @@ async function updateCollaboratorDoc ( } res = res.concat( - await ctx.with( - 'create-collab-docinfo', - {}, - async (ctx) => - await createCollabDocInfo( - ctx, - collabsInfo.result as Ref[], - control, - tx, - originTx, - doc, - activityMessages, - params, - collabsInfo.removed as Ref[] - ) + await ctx.with('create-collab-docinfo', {}, (ctx) => + createCollabDocInfo( + ctx, + collabsInfo.result as Ref[], + control, + tx, + originTx, + doc, + activityMessages, + params, + collabsInfo.removed as Ref[] + ) ) ) } else { - const collaborators = await ctx.with( - 'get-doc-collaborators', - {}, - async (ctx) => await getDocCollaborators(ctx, doc, mixin, control) + const collaborators = await ctx.with('get-doc-collaborators', {}, (ctx) => + getDocCollaborators(ctx, doc, mixin, control) ) res.push(getMixinTx(tx, control, collaborators)) res = res.concat( @@ -1558,14 +1519,12 @@ async function updateCollaboratorDoc ( } res = res.concat( - await ctx.with( - 'get-space-collabtxes', - {}, - async (ctx) => await getSpaceCollabTxes(ctx, control, doc, tx, originTx, activityMessages, cache) + await ctx.with('get-space-collabtxes', {}, (ctx) => + getSpaceCollabTxes(ctx, control, doc, tx, originTx, activityMessages, cache) ) ) res = res.concat( - await ctx.with('update-notify-context-space', {}, async (ctx) => await updateNotifyContextsSpace(ctx, control, tx)) + await ctx.with('update-notify-context-space', {}, (ctx) => updateNotifyContextsSpace(ctx, control, tx)) ) return res @@ -1574,54 +1533,70 @@ async function updateCollaboratorDoc ( /** * @public */ -export async function OnAttributeCreate (tx: Tx, control: TriggerControl): Promise { - const attribute = TxProcessor.createDoc2Doc(tx as TxCreateDoc) - const group = ( - await control.modelDb.findAll(notification.class.NotificationGroup, { objectClass: attribute.attributeOf }) - )[0] - if (group === undefined) return [] - const isCollection: boolean = core.class.Collection === attribute.type._class - const objectClass = !isCollection ? attribute.attributeOf : (attribute.type as Collection).of - const txClasses = !isCollection - ? [control.hierarchy.isMixin(attribute.attributeOf) ? core.class.TxMixin : core.class.TxUpdateDoc] - : [core.class.TxCreateDoc, core.class.TxRemoveDoc] - const data: Data = { - attribute: attribute._id, - group: group._id, - field: attribute.name, - generated: true, - objectClass, - txClasses, - hidden: false, - defaultEnabled: false, - templates: { - textTemplate: '{body}', - htmlTemplate: '

{body}

', - subjectTemplate: '{doc} updated' - }, - label: attribute.label +export async function OnAttributeCreate (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + const attribute = TxProcessor.createDoc2Doc(tx as TxCreateDoc) + const group = ( + await control.modelDb.findAll(notification.class.NotificationGroup, { objectClass: attribute.attributeOf }) + )[0] + if (group === undefined) { + continue + } + const isCollection: boolean = core.class.Collection === attribute.type._class + const objectClass = !isCollection ? attribute.attributeOf : (attribute.type as Collection).of + const txClasses = !isCollection + ? [control.hierarchy.isMixin(attribute.attributeOf) ? core.class.TxMixin : core.class.TxUpdateDoc] + : [core.class.TxCreateDoc, core.class.TxRemoveDoc] + const data: Data = { + attribute: attribute._id, + group: group._id, + field: attribute.name, + generated: true, + objectClass, + txClasses, + hidden: false, + defaultEnabled: false, + templates: { + textTemplate: '{body}', + htmlTemplate: '

{body}

', + subjectTemplate: '{doc} updated' + }, + label: attribute.label + } + if (isCollection) { + data.attachedToClass = attribute.attributeOf + } + const id = + `${notification.class.NotificationType}_${attribute.attributeOf}_${attribute.name}` as Ref + result.push(control.txFactory.createTxCreateDoc(notification.class.NotificationType, core.space.Model, data, id)) } - if (isCollection) { - data.attachedToClass = attribute.attributeOf - } - const id = - `${notification.class.NotificationType}_${attribute.attributeOf}_${attribute.name}` as Ref - const res = control.txFactory.createTxCreateDoc(notification.class.NotificationType, core.space.Model, data, id) - return [res] + return result } /** * @public */ -export async function OnAttributeUpdate (tx: Tx, control: TriggerControl): Promise { - const ctx = tx as TxUpdateDoc - if (ctx.operations.hidden === undefined) return [] - const type = (await control.findAll(control.ctx, notification.class.NotificationType, { attribute: ctx.objectId }))[0] - if (type === undefined) return [] - const res = control.txFactory.createTxUpdateDoc(type._class, type.space, type._id, { - hidden: ctx.operations.hidden - }) - return [res] +export async function OnAttributeUpdate (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + const ctx = tx as TxUpdateDoc + if (ctx.operations.hidden === undefined) { + continue + } + const type = ( + await control.findAll(control.ctx, notification.class.NotificationType, { attribute: ctx.objectId }) + )[0] + if (type === undefined) { + continue + } + result.push( + control.txFactory.createTxUpdateDoc(type._class, type.space, type._id, { + hidden: ctx.operations.hidden + }) + ) + } + return result } async function applyUserTxes ( @@ -1755,10 +1730,8 @@ export async function createCollaboratorNotifications ( ): Promise { if (tx.space === core.space.DerivedTx) { // do not forgot update collaborators for derived tx - return await ctx.with( - 'updateDerivedCollaborators', - {}, - async (ctx) => await updateCollaborators(ctx, control, TxProcessor.extractTx(tx) as TxCUD, cache) + return await ctx.with('updateDerivedCollaborators', {}, (ctx) => + updateCollaborators(ctx, control, TxProcessor.extractTx(tx) as TxCUD, cache) ) } @@ -1768,45 +1741,27 @@ export async function createCollaboratorNotifications ( switch (tx._class) { case core.class.TxCreateDoc: { - const res = await ctx.with( - 'createCollaboratorDoc', - {}, - async (ctx) => - await createCollaboratorDoc(ctx, tx as TxCreateDoc, control, activityMessages, originTx ?? tx, cache) + const res = await ctx.with('createCollaboratorDoc', {}, (ctx) => + createCollaboratorDoc(ctx, tx as TxCreateDoc, control, activityMessages, originTx ?? tx, cache) ) return await applyUserTxes(ctx, control, res) } case core.class.TxUpdateDoc: case core.class.TxMixin: { - let res = await ctx.with( - 'updateCollaboratorDoc', - {}, - async (ctx) => - await updateCollaboratorDoc(ctx, tx as TxUpdateDoc, control, originTx ?? tx, activityMessages, cache) + let res = await ctx.with('updateCollaboratorDoc', {}, (ctx) => + updateCollaboratorDoc(ctx, tx as TxUpdateDoc, control, originTx ?? tx, activityMessages, cache) ) res = res.concat( - await ctx.with( - 'updateCollaboratorMixin', - {}, - async (ctx) => - await updateCollaboratorsMixin( - ctx, - tx as TxMixin, - control, - activityMessages, - originTx ?? tx - ) + await ctx.with('updateCollaboratorMixin', {}, (ctx) => + updateCollaboratorsMixin(ctx, tx as TxMixin, control, activityMessages, originTx ?? tx) ) ) return await applyUserTxes(ctx, control, res) } case core.class.TxCollectionCUD: { - const res = await ctx.with( - 'collectionCollabDoc', - {}, - async (ctx) => - await collectionCollabDoc(ctx, tx as TxCollectionCUD, control, activityMessages, cache) + const res = await ctx.with('collectionCollabDoc', {}, (ctx) => + collectionCollabDoc(ctx, tx as TxCollectionCUD, control, activityMessages, cache) ) return await applyUserTxes(ctx, control, res) } @@ -1914,29 +1869,33 @@ async function OnActivityMessageRemove (message: ActivityMessage, control: Trigg return res } -async function OnEmployeeDeactivate (tx: TxCUD, control: TriggerControl): Promise { - const actualTx = TxProcessor.extractTx(tx) - if (core.class.TxMixin !== actualTx._class) { - return [] - } - const ctx = actualTx as TxMixin - if (ctx.mixin !== contact.mixin.Employee || ctx.attributes.active !== false) { - return [] - } +async function OnEmployeeDeactivate (txes: TxCUD[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + const actualTx = TxProcessor.extractTx(tx) + if (core.class.TxMixin !== actualTx._class) { + continue + } + const ctx = actualTx as TxMixin + if (ctx.mixin !== contact.mixin.Employee || ctx.attributes.active !== false) { + continue + } - const targetAccount = control.modelDb.getAccountByPersonId(ctx.objectId) as PersonAccount[] - if (targetAccount.length === 0) return [] + const targetAccount = control.modelDb.getAccountByPersonId(ctx.objectId) as PersonAccount[] + if (targetAccount.length === 0) { + continue + } - const res: Tx[] = [] - for (const acc of targetAccount) { - const subscriptions = await control.findAll(control.ctx, notification.class.PushSubscription, { - user: acc._id - }) - for (const sub of subscriptions) { - res.push(control.txFactory.createTxRemoveDoc(sub._class, sub.space, sub._id)) + for (const acc of targetAccount) { + const subscriptions = await control.findAll(control.ctx, notification.class.PushSubscription, { + user: acc._id + }) + for (const sub of subscriptions) { + result.push(control.txFactory.createTxRemoveDoc(sub._class, sub.space, sub._id)) + } } } - return res + return result } async function OnDocRemove (txes: TxCUD[], control: TriggerControl): Promise { diff --git a/server-plugins/recruit-resources/src/index.ts b/server-plugins/recruit-resources/src/index.ts index b7dd1bef11..23fe554d0e 100644 --- a/server-plugins/recruit-resources/src/index.ts +++ b/server-plugins/recruit-resources/src/index.ts @@ -84,22 +84,23 @@ export async function applicationTextPresenter (doc: Doc, control: TriggerContro /** * @public */ -export async function OnRecruitUpdate (tx: Tx, control: TriggerControl): Promise { - const actualTx = TxProcessor.extractTx(tx) as TxCUD - if (!control.hierarchy.isDerived(actualTx.objectClass, recruit.class.Vacancy)) { - return [] - } +export async function OnRecruitUpdate (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + const actualTx = TxProcessor.extractTx(tx) as TxCUD + if (!control.hierarchy.isDerived(actualTx.objectClass, recruit.class.Vacancy)) { + continue + } - const res: Tx[] = [] - - if (actualTx._class === core.class.TxCreateDoc) { - handleVacancyCreate(control, actualTx, res) - } else if (actualTx._class === core.class.TxUpdateDoc) { - await handleVacancyUpdate(control, actualTx, res) - } else if (actualTx._class === core.class.TxRemoveDoc) { - handleVacancyRemove(control, actualTx, res) + if (actualTx._class === core.class.TxCreateDoc) { + handleVacancyCreate(control, actualTx, result) + } else if (actualTx._class === core.class.TxUpdateDoc) { + await handleVacancyUpdate(control, actualTx, result) + } else if (actualTx._class === core.class.TxRemoveDoc) { + handleVacancyRemove(control, actualTx, result) + } } - return res + return result } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type diff --git a/server-plugins/setting-resources/src/index.ts b/server-plugins/setting-resources/src/index.ts index 135a642771..78e2e459fd 100644 --- a/server-plugins/setting-resources/src/index.ts +++ b/server-plugins/setting-resources/src/index.ts @@ -92,23 +92,31 @@ export async function getOwnerPosition ( /** * @public */ -export async function OnRoleNameUpdate (tx: Tx, control: TriggerControl): Promise { - const actualTx = TxProcessor.extractTx(tx) - const updateTx = actualTx as TxUpdateDoc +export async function OnRoleNameUpdate (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + const actualTx = TxProcessor.extractTx(tx) + const updateTx = actualTx as TxUpdateDoc - if (updateTx.operations?.name === undefined) return [] + if (updateTx.operations?.name === undefined) { + continue + } - // Update the related mixin attribute - const roleAttribute = await control.modelDb.findOne(core.class.Attribute, { - name: updateTx.objectId - }) - if (roleAttribute === undefined) return [] + // Update the related mixin attribute + const roleAttribute = await control.modelDb.findOne(core.class.Attribute, { + name: updateTx.objectId + }) + if (roleAttribute === undefined) { + continue + } - const updAttrTx = control.txFactory.createTxUpdateDoc(core.class.Attribute, core.space.Model, roleAttribute._id, { - label: getEmbeddedLabel(updateTx.operations.name) - }) - - return [updAttrTx] + result.push( + control.txFactory.createTxUpdateDoc(core.class.Attribute, core.space.Model, roleAttribute._id, { + label: getEmbeddedLabel(updateTx.operations.name) + }) + ) + } + return result } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type diff --git a/server-plugins/tags-resources/src/index.ts b/server-plugins/tags-resources/src/index.ts index 1e65bbe41c..415e69a3c9 100644 --- a/server-plugins/tags-resources/src/index.ts +++ b/server-plugins/tags-resources/src/index.ts @@ -49,7 +49,7 @@ export async function TagElementRemove ( /** * @public */ -export async function onTagReference (txes: Tx[], control: TriggerControl): Promise { +export async function OnTagReference (txes: Tx[], control: TriggerControl): Promise { const result: Tx[] = [] for (const tx of txes) { const actualTx = TxProcessor.extractTx(tx) @@ -89,7 +89,7 @@ export async function onTagReference (txes: Tx[], control: TriggerControl): Prom // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export default async () => ({ trigger: { - onTagReference + onTagReference: OnTagReference }, function: { TagElementRemove diff --git a/server-plugins/telegram-resources/src/index.ts b/server-plugins/telegram-resources/src/index.ts index bc62bd1101..efde2ae8d8 100644 --- a/server-plugins/telegram-resources/src/index.ts +++ b/server-plugins/telegram-resources/src/index.ts @@ -75,23 +75,24 @@ export async function FindMessages ( /** * @public */ -export async function OnMessageCreate (tx: Tx, control: TriggerControl): Promise { - const res: Tx[] = [] - - const message = TxProcessor.createDoc2Doc(tx as TxCreateDoc) - const channel = ( - await control.findAll(control.ctx, contact.class.Channel, { _id: message.attachedTo }, { limit: 1 }) - )[0] - if (channel !== undefined) { - if (channel.lastMessage === undefined || channel.lastMessage < message.sendOn) { - const tx = control.txFactory.createTxUpdateDoc(channel._class, channel.space, channel._id, { - lastMessage: message.sendOn - }) - res.push(tx) +export async function OnMessageCreate (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + const message = TxProcessor.createDoc2Doc(tx as TxCreateDoc) + const channel = ( + await control.findAll(control.ctx, contact.class.Channel, { _id: message.attachedTo }, { limit: 1 }) + )[0] + if (channel !== undefined) { + if (channel.lastMessage === undefined || channel.lastMessage < message.sendOn) { + const tx = control.txFactory.createTxUpdateDoc(channel._class, channel.space, channel._id, { + lastMessage: message.sendOn + }) + result.push(tx) + } } } - return res + return result } /** diff --git a/server-plugins/time-resources/src/index.ts b/server-plugins/time-resources/src/index.ts index 324011110c..f97c857472 100644 --- a/server-plugins/time-resources/src/index.ts +++ b/server-plugins/time-resources/src/index.ts @@ -72,62 +72,88 @@ export async function OnTask (txes: Tx[], control: TriggerControl): Promise { - const actualTx = TxProcessor.extractTx(tx) as TxCUD - if (!control.hierarchy.isDerived(actualTx.objectClass, time.class.WorkSlot)) return [] - if (!control.hierarchy.isDerived(actualTx._class, core.class.TxUpdateDoc)) return [] - const updTx = actualTx as TxUpdateDoc - const visibility = updTx.operations.visibility - if (visibility !== undefined) { - const workslot = (await control.findAll(control.ctx, time.class.WorkSlot, { _id: updTx.objectId }, { limit: 1 }))[0] - if (workslot === undefined) return [] +export async function OnWorkSlotUpdate (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + const actualTx = TxProcessor.extractTx(tx) as TxCUD + if (!control.hierarchy.isDerived(actualTx.objectClass, time.class.WorkSlot)) { + continue + } + if (!control.hierarchy.isDerived(actualTx._class, core.class.TxUpdateDoc)) { + continue + } + const updTx = actualTx as TxUpdateDoc + const visibility = updTx.operations.visibility + if (visibility !== undefined) { + const workslot = ( + await control.findAll(control.ctx, time.class.WorkSlot, { _id: updTx.objectId }, { limit: 1 }) + )[0] + if (workslot === undefined) { + continue + } + const todo = (await control.findAll(control.ctx, time.class.ToDo, { _id: workslot.attachedTo }))[0] + result.push(control.txFactory.createTxUpdateDoc(todo._class, todo.space, todo._id, { visibility })) + } + } + return result +} + +export async function OnWorkSlotCreate (txes: Tx[], control: TriggerControl): Promise { + for (const tx of txes) { + const actualTx = TxProcessor.extractTx(tx) as TxCUD + if (!control.hierarchy.isDerived(actualTx.objectClass, time.class.WorkSlot)) { + continue + } + if (!control.hierarchy.isDerived(actualTx._class, core.class.TxCreateDoc)) { + continue + } + const workslot = TxProcessor.createDoc2Doc(actualTx as TxCreateDoc) + const workslots = await control.findAll(control.ctx, time.class.WorkSlot, { attachedTo: workslot.attachedTo }) + if (workslots.length > 1) { + continue + } const todo = (await control.findAll(control.ctx, time.class.ToDo, { _id: workslot.attachedTo }))[0] - return [control.txFactory.createTxUpdateDoc(todo._class, todo.space, todo._id, { visibility })] - } - return [] -} - -export async function OnWorkSlotCreate (tx: Tx, control: TriggerControl): Promise { - const actualTx = TxProcessor.extractTx(tx) as TxCUD - if (!control.hierarchy.isDerived(actualTx.objectClass, time.class.WorkSlot)) return [] - if (!control.hierarchy.isDerived(actualTx._class, core.class.TxCreateDoc)) return [] - const workslot = TxProcessor.createDoc2Doc(actualTx as TxCreateDoc) - const workslots = await control.findAll(control.ctx, time.class.WorkSlot, { attachedTo: workslot.attachedTo }) - if (workslots.length > 1) return [] - const todo = (await control.findAll(control.ctx, time.class.ToDo, { _id: workslot.attachedTo }))[0] - if (todo === undefined) return [] - if (!control.hierarchy.isDerived(todo.attachedToClass, tracker.class.Issue)) return [] - const issue = (await control.findAll(control.ctx, tracker.class.Issue, { _id: todo.attachedTo as Ref }))[0] - if (issue === undefined) return [] - const project = (await control.findAll(control.ctx, task.class.Project, { _id: issue.space }))[0] - if (project !== undefined) { - const type = (await control.modelDb.findAll(task.class.ProjectType, { _id: project.type }))[0] - if (type?.classic) { - const taskType = (await control.modelDb.findAll(task.class.TaskType, { _id: issue.kind }))[0] - if (taskType !== undefined) { - const statuses = await control.modelDb.findAll(core.class.Status, { _id: { $in: taskType.statuses } }) - const statusMap = toIdMap(statuses) - const typeStatuses = taskType.statuses.map((p) => statusMap.get(p)).filter((p) => p !== undefined) as Status[] - const current = statusMap.get(issue.status) - if (current === undefined) return [] - if (current.category !== task.statusCategory.UnStarted && current.category !== task.statusCategory.ToDo) { - return [] - } - const nextStatus = typeStatuses.find((p) => p.category === task.statusCategory.Active) - if (nextStatus !== undefined) { - const factory = new TxFactory(control.txFactory.account) - const innerTx = factory.createTxUpdateDoc(issue._class, issue.space, issue._id, { - status: nextStatus._id - }) - const outerTx = factory.createTxCollectionCUD( - issue.attachedToClass, - issue.attachedTo, - issue.space, - issue.collection, - innerTx - ) - await control.apply(control.ctx, [outerTx]) - return [] + if (todo === undefined) { + continue + } + if (!control.hierarchy.isDerived(todo.attachedToClass, tracker.class.Issue)) { + continue + } + const issue = (await control.findAll(control.ctx, tracker.class.Issue, { _id: todo.attachedTo as Ref }))[0] + if (issue === undefined) { + continue + } + const project = (await control.findAll(control.ctx, task.class.Project, { _id: issue.space }))[0] + if (project !== undefined) { + const type = (await control.modelDb.findAll(task.class.ProjectType, { _id: project.type }))[0] + if (type?.classic) { + const taskType = (await control.modelDb.findAll(task.class.TaskType, { _id: issue.kind }))[0] + if (taskType !== undefined) { + const statuses = await control.modelDb.findAll(core.class.Status, { _id: { $in: taskType.statuses } }) + const statusMap = toIdMap(statuses) + const typeStatuses = taskType.statuses.map((p) => statusMap.get(p)).filter((p) => p !== undefined) as Status[] + const current = statusMap.get(issue.status) + if (current === undefined) { + continue + } + if (current.category !== task.statusCategory.UnStarted && current.category !== task.statusCategory.ToDo) { + continue + } + const nextStatus = typeStatuses.find((p) => p.category === task.statusCategory.Active) + if (nextStatus !== undefined) { + const factory = new TxFactory(control.txFactory.account) + const innerTx = factory.createTxUpdateDoc(issue._class, issue.space, issue._id, { + status: nextStatus._id + }) + const outerTx = factory.createTxCollectionCUD( + issue.attachedToClass, + issue.attachedTo, + issue.space, + issue.collection, + innerTx + ) + await control.apply(control.ctx, [outerTx]) + } } } } @@ -135,45 +161,62 @@ export async function OnWorkSlotCreate (tx: Tx, control: TriggerControl): Promis return [] } -export async function OnToDoRemove (tx: Tx, control: TriggerControl): Promise { - const actualTx = TxProcessor.extractTx(tx) as TxCUD - if (!control.hierarchy.isDerived(actualTx.objectClass, time.class.ToDo)) return [] - if (!control.hierarchy.isDerived(actualTx._class, core.class.TxRemoveDoc)) return [] - const todo = control.removedMap.get(actualTx.objectId) as ToDo - if (todo === undefined) return [] - // it was closed, do nothing - if (todo.doneOn != null) return [] - const todos = await control.findAll(control.ctx, time.class.ToDo, { attachedTo: todo.attachedTo }) - if (todos.length > 0) return [] - const issue = (await control.findAll(control.ctx, tracker.class.Issue, { _id: todo.attachedTo as Ref }))[0] - if (issue === undefined) return [] - const project = (await control.findAll(control.ctx, task.class.Project, { _id: issue.space }))[0] - if (project !== undefined) { - const type = (await control.modelDb.findAll(task.class.ProjectType, { _id: project.type }))[0] - if (type !== undefined && type.classic) { - const factory = new TxFactory(control.txFactory.account) - const taskType = (await control.modelDb.findAll(task.class.TaskType, { _id: issue.kind }))[0] - if (taskType !== undefined) { - const statuses = await control.modelDb.findAll(core.class.Status, { _id: { $in: taskType.statuses } }) - const statusMap = toIdMap(statuses) - const typeStatuses = taskType.statuses.map((p) => statusMap.get(p)).filter((p) => p !== undefined) as Status[] - const current = statusMap.get(issue.status) - if (current === undefined) return [] - if (current.category !== task.statusCategory.Active && current.category !== task.statusCategory.ToDo) return [] - const nextStatus = typeStatuses.find((p) => p.category === task.statusCategory.UnStarted) - if (nextStatus !== undefined) { - const innerTx = factory.createTxUpdateDoc(issue._class, issue.space, issue._id, { - status: nextStatus._id - }) - const outerTx = factory.createTxCollectionCUD( - issue.attachedToClass, - issue.attachedTo, - issue.space, - issue.collection, - innerTx - ) - await control.apply(control.ctx, [outerTx]) - return [] +export async function OnToDoRemove (txes: Tx[], control: TriggerControl): Promise { + for (const tx of txes) { + const actualTx = TxProcessor.extractTx(tx) as TxCUD + if (!control.hierarchy.isDerived(actualTx.objectClass, time.class.ToDo)) { + continue + } + if (!control.hierarchy.isDerived(actualTx._class, core.class.TxRemoveDoc)) { + continue + } + const todo = control.removedMap.get(actualTx.objectId) as ToDo + if (todo === undefined) { + continue + } + // it was closed, do nothing + if (todo.doneOn != null) { + continue + } + const todos = await control.findAll(control.ctx, time.class.ToDo, { attachedTo: todo.attachedTo }) + if (todos.length > 0) { + continue + } + const issue = (await control.findAll(control.ctx, tracker.class.Issue, { _id: todo.attachedTo as Ref }))[0] + if (issue === undefined) { + continue + } + const project = (await control.findAll(control.ctx, task.class.Project, { _id: issue.space }))[0] + if (project !== undefined) { + const type = (await control.modelDb.findAll(task.class.ProjectType, { _id: project.type }))[0] + if (type !== undefined && type.classic) { + const factory = new TxFactory(control.txFactory.account) + const taskType = (await control.modelDb.findAll(task.class.TaskType, { _id: issue.kind }))[0] + if (taskType !== undefined) { + const statuses = await control.modelDb.findAll(core.class.Status, { _id: { $in: taskType.statuses } }) + const statusMap = toIdMap(statuses) + const typeStatuses = taskType.statuses.map((p) => statusMap.get(p)).filter((p) => p !== undefined) as Status[] + const current = statusMap.get(issue.status) + if (current === undefined) { + continue + } + if (current.category !== task.statusCategory.Active && current.category !== task.statusCategory.ToDo) { + continue + } + const nextStatus = typeStatuses.find((p) => p.category === task.statusCategory.UnStarted) + if (nextStatus !== undefined) { + const innerTx = factory.createTxUpdateDoc(issue._class, issue.space, issue._id, { + status: nextStatus._id + }) + const outerTx = factory.createTxCollectionCUD( + issue.attachedToClass, + issue.attachedTo, + issue.space, + issue.collection, + innerTx + ) + await control.apply(control.ctx, [outerTx]) + } } } } @@ -181,102 +224,114 @@ export async function OnToDoRemove (tx: Tx, control: TriggerControl): Promise, control: TriggerControl): Promise { +export async function OnToDoCreate (txes: TxCUD[], control: TriggerControl): Promise { const hierarchy = control.hierarchy - const createTx = TxProcessor.extractTx(tx) as TxCreateDoc + for (const tx of txes) { + const createTx = TxProcessor.extractTx(tx) as TxCreateDoc - if (!hierarchy.isDerived(createTx.objectClass, time.class.ToDo)) return [] - if (!hierarchy.isDerived(createTx._class, core.class.TxCreateDoc)) return [] + if (!hierarchy.isDerived(createTx.objectClass, time.class.ToDo)) { + continue + } + if (!hierarchy.isDerived(createTx._class, core.class.TxCreateDoc)) { + continue + } - const mixin = hierarchy.classHierarchyMixin( - createTx.objectClass as Ref>, - notification.mixin.ClassCollaborators - ) - - if (mixin === undefined) { - return [] - } - - const todo = TxProcessor.createDoc2Doc(createTx) - const account = control.modelDb.getAccountByPersonId(todo.user) as PersonAccount[] - - if (account.length === 0) { - return [] - } - - const object = (await control.findAll(control.ctx, todo.attachedToClass, { _id: todo.attachedTo }))[0] - if (object === undefined) return [] - - const person = ( - await control.findAll( - control.ctx, - contact.mixin.Employee, - { _id: todo.user as Ref, active: true }, - { limit: 1 } + const mixin = hierarchy.classHierarchyMixin( + createTx.objectClass as Ref>, + notification.mixin.ClassCollaborators ) - )[0] - if (person === undefined) return [] - const personSpace = ( - await control.findAll(control.ctx, contact.class.PersonSpace, { person: todo.user }, { limit: 1 }) - )[0] - if (personSpace === undefined) return [] + if (mixin === undefined) { + continue + } - // TODO: Select a proper account - const receiverInfo: ReceiverInfo = { - _id: account[0]._id, - account: account[0], - person, - space: personSpace._id - } + const todo = TxProcessor.createDoc2Doc(createTx) + const account = control.modelDb.getAccountByPersonId(todo.user) as PersonAccount[] - const senderAccount = control.modelDb.findAllSync(contact.class.PersonAccount, { - _id: tx.modifiedBy as Ref - })[0] - const senderPerson = - senderAccount !== undefined - ? (await control.findAll(control.ctx, contact.class.Person, { _id: senderAccount.person }))[0] - : undefined + if (account.length === 0) { + continue + } - const senderInfo: SenderInfo = { - _id: tx.modifiedBy, - account: senderAccount, - person: senderPerson - } - const notificationControl = await getNotificationProviderControl(control.ctx, control) - const notifyResult = await isShouldNotifyTx(control, createTx, tx, todo, account, true, false, notificationControl) - const content = await getNotificationContent(tx, account, senderInfo, todo, control) - const data: Partial> = { - ...content, - header: time.string.ToDo, - headerIcon: time.icon.Planned, - headerObjectId: object._id, - headerObjectClass: object._class, - messageHtml: jsonToMarkup(nodeDoc(nodeParagraph(nodeText(todo.title)))) - } + const object = (await control.findAll(control.ctx, todo.attachedToClass, { _id: todo.attachedTo }))[0] + if (object === undefined) { + continue + } - const txes = await getCommonNotificationTxes( - control.ctx, - control, - object, - data, - receiverInfo, - senderInfo, - object._id, - object._class, - object.space, - createTx.modifiedOn, - notifyResult, - notification.class.CommonInboxNotification, - tx - ) + const person = ( + await control.findAll( + control.ctx, + contact.mixin.Employee, + { _id: todo.user as Ref, active: true }, + { limit: 1 } + ) + )[0] + if (person === undefined) { + continue + } - await control.apply(control.ctx, txes) + const personSpace = ( + await control.findAll(control.ctx, contact.class.PersonSpace, { person: todo.user }, { limit: 1 }) + )[0] + if (personSpace === undefined) { + continue + } - const ids = txes.map((it) => it._id) - control.ctx.contextData.broadcast.targets.notifications = (it) => { - if (ids.includes(it._id)) { - return [receiverInfo.account.email] + // TODO: Select a proper account + const receiverInfo: ReceiverInfo = { + _id: account[0]._id, + account: account[0], + person, + space: personSpace._id + } + + const senderAccount = control.modelDb.findAllSync(contact.class.PersonAccount, { + _id: tx.modifiedBy as Ref + })[0] + const senderPerson = + senderAccount !== undefined + ? (await control.findAll(control.ctx, contact.class.Person, { _id: senderAccount.person }))[0] + : undefined + + const senderInfo: SenderInfo = { + _id: tx.modifiedBy, + account: senderAccount, + person: senderPerson + } + const notificationControl = await getNotificationProviderControl(control.ctx, control) + const notifyResult = await isShouldNotifyTx(control, createTx, tx, todo, account, true, false, notificationControl) + const content = await getNotificationContent(tx, account, senderInfo, todo, control) + const data: Partial> = { + ...content, + header: time.string.ToDo, + headerIcon: time.icon.Planned, + headerObjectId: object._id, + headerObjectClass: object._class, + messageHtml: jsonToMarkup(nodeDoc(nodeParagraph(nodeText(todo.title)))) + } + + const txes = await getCommonNotificationTxes( + control.ctx, + control, + object, + data, + receiverInfo, + senderInfo, + object._id, + object._class, + object.space, + createTx.modifiedOn, + notifyResult, + notification.class.CommonInboxNotification, + tx + ) + + await control.apply(control.ctx, txes) + + const ids = txes.map((it) => it._id) + control.ctx.contextData.broadcast.targets.notifications = (it) => { + if (ids.includes(it._id)) { + return [receiverInfo.account.email] + } } } return [] @@ -285,40 +340,93 @@ export async function OnToDoCreate (tx: TxCUD, control: TriggerControl): Pr /** * @public */ -export async function OnToDoUpdate (tx: Tx, control: TriggerControl): Promise { - const actualTx = TxProcessor.extractTx(tx) as TxCUD - if (!control.hierarchy.isDerived(actualTx.objectClass, time.class.ToDo)) return [] - if (!control.hierarchy.isDerived(actualTx._class, core.class.TxUpdateDoc)) return [] - const updTx = actualTx as TxUpdateDoc - const doneOn = updTx.operations.doneOn - const title = updTx.operations.title - const description = updTx.operations.description - const visibility = updTx.operations.visibility - if (doneOn != null) { - const events = await control.findAll(control.ctx, time.class.WorkSlot, { attachedTo: updTx.objectId }) - const res: Tx[] = [] - const resEvents: WorkSlot[] = [] - for (const event of events) { - if (event.date > doneOn) { - const innerTx = control.txFactory.createTxRemoveDoc(event._class, event.space, event._id) - const outerTx = control.txFactory.createTxCollectionCUD( - event.attachedToClass, - event.attachedTo, - event.space, - event.collection, - innerTx - ) - res.push(outerTx) - } else if (event.dueDate > doneOn) { - const upd: DocumentUpdate = { - dueDate: doneOn +export async function OnToDoUpdate (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + const actualTx = TxProcessor.extractTx(tx) as TxCUD + if (!control.hierarchy.isDerived(actualTx.objectClass, time.class.ToDo)) { + continue + } + if (!control.hierarchy.isDerived(actualTx._class, core.class.TxUpdateDoc)) { + continue + } + const updTx = actualTx as TxUpdateDoc + const doneOn = updTx.operations.doneOn + const title = updTx.operations.title + const description = updTx.operations.description + const visibility = updTx.operations.visibility + if (doneOn != null) { + const events = await control.findAll(control.ctx, time.class.WorkSlot, { attachedTo: updTx.objectId }) + const resEvents: WorkSlot[] = [] + for (const event of events) { + if (event.date > doneOn) { + const innerTx = control.txFactory.createTxRemoveDoc(event._class, event.space, event._id) + const outerTx = control.txFactory.createTxCollectionCUD( + event.attachedToClass, + event.attachedTo, + event.space, + event.collection, + innerTx + ) + result.push(outerTx) + } else if (event.dueDate > doneOn) { + const upd: DocumentUpdate = { + dueDate: doneOn + } + if (title !== undefined) { + upd.title = title + } + if (description !== undefined) { + upd.description = description + } + const innerTx = control.txFactory.createTxUpdateDoc(event._class, event.space, event._id, upd) + const outerTx = control.txFactory.createTxCollectionCUD( + event.attachedToClass, + event.attachedTo, + event.space, + event.collection, + innerTx + ) + result.push(outerTx) + resEvents.push({ + ...event, + dueDate: doneOn + }) + } else { + resEvents.push(event) } + } + const todo = (await control.findAll(control.ctx, time.class.ToDo, { _id: updTx.objectId }))[0] + if (todo === undefined) { + continue + } + const funcs = control.hierarchy.classHierarchyMixin, OnToDo>( + todo.attachedToClass, + serverTime.mixin.OnToDo + ) + if (funcs !== undefined) { + const func = await getResource(funcs.onDone) + const todoRes = await func(control, resEvents, todo) + await control.apply(control.ctx, todoRes) + } + continue + } + if (title !== undefined || description !== undefined || visibility !== undefined) { + const events = await control.findAll(control.ctx, time.class.WorkSlot, { attachedTo: updTx.objectId }) + for (const event of events) { + const upd: DocumentUpdate = {} if (title !== undefined) { upd.title = title } if (description !== undefined) { upd.description = description } + if (visibility !== undefined) { + const newVisibility = visibility === 'public' ? 'public' : 'freeBusy' + if (event.visibility !== newVisibility) { + upd.visibility = newVisibility + } + } const innerTx = control.txFactory.createTxUpdateDoc(event._class, event.space, event._id, upd) const outerTx = control.txFactory.createTxCollectionCUD( event.attachedToClass, @@ -327,58 +435,11 @@ export async function OnToDoUpdate (tx: Tx, control: TriggerControl): Promise, OnToDo>( - todo.attachedToClass, - serverTime.mixin.OnToDo - ) - if (funcs !== undefined) { - const func = await getResource(funcs.onDone) - const todoRes = await func(control, resEvents, todo) - await control.apply(control.ctx, todoRes) - } - return res } - if (title !== undefined || description !== undefined || visibility !== undefined) { - const events = await control.findAll(control.ctx, time.class.WorkSlot, { attachedTo: updTx.objectId }) - const res: Tx[] = [] - for (const event of events) { - const upd: DocumentUpdate = {} - if (title !== undefined) { - upd.title = title - } - if (description !== undefined) { - upd.description = description - } - if (visibility !== undefined) { - const newVisibility = visibility === 'public' ? 'public' : 'freeBusy' - if (event.visibility !== newVisibility) { - upd.visibility = newVisibility - } - } - const innerTx = control.txFactory.createTxUpdateDoc(event._class, event.space, event._id, upd) - const outerTx = control.txFactory.createTxCollectionCUD( - event.attachedToClass, - event.attachedTo, - event.space, - event.collection, - innerTx - ) - res.push(outerTx) - } - return res - } - return [] + return result } /** diff --git a/server-plugins/tracker-resources/src/index.ts b/server-plugins/tracker-resources/src/index.ts index dc3e821f2d..6b2660ef69 100644 --- a/server-plugins/tracker-resources/src/index.ts +++ b/server-plugins/tracker-resources/src/index.ts @@ -13,6 +13,8 @@ // limitations under the License. // +import chunter, { ChatMessage } from '@hcengineering/chunter' +import { Person, PersonAccount } from '@hcengineering/contact' import core, { Account, AccountRole, @@ -31,15 +33,13 @@ import core, { TxUpdateDoc, WithLookup } from '@hcengineering/core' -import { getMetadata, IntlString } from '@hcengineering/platform' -import { Person, PersonAccount } from '@hcengineering/contact' -import serverCore, { TriggerControl } from '@hcengineering/server-core' -import tracker, { Component, Issue, IssueParentInfo, TimeSpendReport, trackerId } from '@hcengineering/tracker' import { NotificationContent } from '@hcengineering/notification' -import { workbenchId } from '@hcengineering/workbench' -import { stripTags } from '@hcengineering/text' -import chunter, { ChatMessage } from '@hcengineering/chunter' +import { getMetadata, IntlString } from '@hcengineering/platform' +import serverCore, { TriggerControl } from '@hcengineering/server-core' import { NOTIFICATION_BODY_SIZE } from '@hcengineering/server-notification' +import { stripTags } from '@hcengineering/text' +import tracker, { Component, Issue, IssueParentInfo, TimeSpendReport, trackerId } from '@hcengineering/tracker' +import { workbenchId } from '@hcengineering/workbench' async function updateSubIssues ( updateTx: TxUpdateDoc, @@ -160,137 +160,143 @@ export async function getIssueNotificationContent ( /** * @public */ -export async function OnComponentRemove (tx: Tx, control: TriggerControl): Promise { - const ctx = TxProcessor.extractTx(tx) as TxRemoveDoc +export async function OnComponentRemove (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + const ctx = TxProcessor.extractTx(tx) as TxRemoveDoc - const issues = await control.findAll(control.ctx, tracker.class.Issue, { - component: ctx.objectId - }) - if (issues === undefined) return [] - const res: Tx[] = [] - - for (const issue of issues) { - const issuePush = { - ...issue, - component: null + const issues = await control.findAll(control.ctx, tracker.class.Issue, { + component: ctx.objectId + }) + if (issues === undefined) { + continue + } + for (const issue of issues) { + const issuePush = { + ...issue, + component: null + } + const tx = control.txFactory.createTxUpdateDoc(issue._class, issue.space, issue._id, issuePush) + result.push(tx) } - const tx = control.txFactory.createTxUpdateDoc(issue._class, issue.space, issue._id, issuePush) - res.push(tx) } - return res + return result } /** * @public */ -export async function OnWorkspaceOwnerAdded (tx: Tx, control: TriggerControl): Promise { - let ownerId: Ref | undefined - if (control.hierarchy.isDerived(tx._class, core.class.TxCreateDoc)) { - const createTx = tx as TxCreateDoc +export async function OnWorkspaceOwnerAdded (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + let ownerId: Ref | undefined + if (control.hierarchy.isDerived(tx._class, core.class.TxCreateDoc)) { + const createTx = tx as TxCreateDoc - if (createTx.attributes.role === AccountRole.Owner) { - ownerId = createTx.objectId + if (createTx.attributes.role === AccountRole.Owner) { + ownerId = createTx.objectId + } + } else if (control.hierarchy.isDerived(tx._class, core.class.TxUpdateDoc)) { + const updateTx = tx as TxUpdateDoc + + if (updateTx.operations.role === AccountRole.Owner) { + ownerId = updateTx.objectId + } } - } else if (control.hierarchy.isDerived(tx._class, core.class.TxUpdateDoc)) { - const updateTx = tx as TxUpdateDoc - if (updateTx.operations.role === AccountRole.Owner) { - ownerId = updateTx.objectId + if (ownerId === undefined) { + continue } - } - if (ownerId === undefined) { - return [] - } - - const targetProject = ( - await control.findAll(control.ctx, tracker.class.Project, { - _id: tracker.project.DefaultProject - }) - )[0] - - if (targetProject === undefined) { - return [] - } - - if ( - targetProject.owners === undefined || - targetProject.owners.length === 0 || - targetProject.owners[0] === core.account.System - ) { - const updTx = control.txFactory.createTxUpdateDoc(tracker.class.Project, targetProject.space, targetProject._id, { - owners: [ownerId] - }) - return [updTx] - } - - return [] -} - -/** - * @public - */ -export async function OnIssueUpdate (tx: Tx, control: TriggerControl): Promise { - const actualTx = TxProcessor.extractTx(tx) - - // Check TimeReport operations - if ( - actualTx._class === core.class.TxCreateDoc || - actualTx._class === core.class.TxUpdateDoc || - actualTx._class === core.class.TxRemoveDoc - ) { - const cud = actualTx as TxCUD - if (cud.objectClass === tracker.class.TimeSpendReport) { - return await doTimeReportUpdate(cud, tx, control) - } - } - - if (actualTx._class === core.class.TxCreateDoc) { - const createTx = actualTx as TxCreateDoc - if (control.hierarchy.isDerived(createTx.objectClass, tracker.class.Issue)) { - const issue = TxProcessor.createDoc2Doc(createTx) - const res: Tx[] = [] - updateIssueParentEstimations(issue, res, control, [], issue.parents) - - return res - } - } - - if (actualTx._class === core.class.TxUpdateDoc) { - const updateTx = actualTx as TxUpdateDoc - if (control.hierarchy.isDerived(updateTx.objectClass, tracker.class.Issue)) { - return await doIssueUpdate(updateTx, control, tx as TxCollectionCUD) - } - } - if (actualTx._class === core.class.TxRemoveDoc) { - const removeTx = actualTx as TxRemoveDoc - if (control.hierarchy.isDerived(removeTx.objectClass, tracker.class.Issue)) { - const parentIssue = await control.findAll(control.ctx, tracker.class.Issue, { - 'childInfo.childId': removeTx.objectId + const targetProject = ( + await control.findAll(control.ctx, tracker.class.Project, { + _id: tracker.project.DefaultProject }) - const res: Tx[] = [] - const parents: IssueParentInfo[] = parentIssue.map((it) => ({ - parentId: it._id, - parentTitle: it.title, - identifier: it.identifier, - space: it.space - })) - updateIssueParentEstimations( - { - _id: removeTx.objectId, - estimation: 0, - reportedTime: 0, - space: removeTx.space - }, - res, - control, - parents, - [] + )[0] + + if (targetProject === undefined) { + continue + } + + if ( + targetProject.owners === undefined || + targetProject.owners.length === 0 || + targetProject.owners[0] === core.account.System + ) { + result.push( + control.txFactory.createTxUpdateDoc(tracker.class.Project, targetProject.space, targetProject._id, { + owners: [ownerId] + }) ) - return res } } - return [] + return result +} + +/** + * @public + */ +export async function OnIssueUpdate (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + const actualTx = TxProcessor.extractTx(tx) + + // Check TimeReport operations + if ( + actualTx._class === core.class.TxCreateDoc || + actualTx._class === core.class.TxUpdateDoc || + actualTx._class === core.class.TxRemoveDoc + ) { + const cud = actualTx as TxCUD + if (cud.objectClass === tracker.class.TimeSpendReport) { + result.push(...(await doTimeReportUpdate(cud, tx, control))) + } + } + + if (actualTx._class === core.class.TxCreateDoc) { + const createTx = actualTx as TxCreateDoc + if (control.hierarchy.isDerived(createTx.objectClass, tracker.class.Issue)) { + const issue = TxProcessor.createDoc2Doc(createTx) + updateIssueParentEstimations(issue, result, control, [], issue.parents) + continue + } + } + + if (actualTx._class === core.class.TxUpdateDoc) { + const updateTx = actualTx as TxUpdateDoc + if (control.hierarchy.isDerived(updateTx.objectClass, tracker.class.Issue)) { + result.push(...(await doIssueUpdate(updateTx, control, tx as TxCollectionCUD))) + continue + } + } + if (actualTx._class === core.class.TxRemoveDoc) { + const removeTx = actualTx as TxRemoveDoc + if (control.hierarchy.isDerived(removeTx.objectClass, tracker.class.Issue)) { + const parentIssue = await control.findAll(control.ctx, tracker.class.Issue, { + 'childInfo.childId': removeTx.objectId + }) + const parents: IssueParentInfo[] = parentIssue.map((it) => ({ + parentId: it._id, + parentTitle: it.title, + identifier: it.identifier, + space: it.space + })) + updateIssueParentEstimations( + { + _id: removeTx.objectId, + estimation: 0, + reportedTime: 0, + space: removeTx.space + }, + result, + control, + parents, + [] + ) + } + } + } + return result } async function doTimeReportUpdate (cud: TxCUD, tx: Tx, control: TriggerControl): Promise { diff --git a/server-plugins/view-resources/src/index.ts b/server-plugins/view-resources/src/index.ts index 10267a751f..27524bcd2a 100644 --- a/server-plugins/view-resources/src/index.ts +++ b/server-plugins/view-resources/src/index.ts @@ -20,24 +20,30 @@ import view from '@hcengineering/view' /** * @public */ -export async function OnCustomAttributeRemove (tx: Tx, control: TriggerControl): Promise { - const hierarchy = control.hierarchy - const ptx = tx as TxRemoveDoc - if (!checkTx(ptx, hierarchy)) return [] - const attribute = control.removedMap.get(ptx.objectId) as AnyAttribute - if (attribute === undefined) return [] - const preferences = await control.findAll(control.ctx, view.class.ViewletPreference, { - config: attribute.name, - space: core.space.Workspace - }) - const res: Tx[] = [] - for (const preference of preferences) { - const tx = control.txFactory.createTxUpdateDoc(preference._class, preference.space, preference._id, { - $pull: { config: attribute.name } +export async function OnCustomAttributeRemove (txes: Tx[], control: TriggerControl): Promise { + const result: Tx[] = [] + for (const tx of txes) { + const hierarchy = control.hierarchy + const ptx = tx as TxRemoveDoc + if (!checkTx(ptx, hierarchy)) { + continue + } + const attribute = control.removedMap.get(ptx.objectId) as AnyAttribute + if (attribute === undefined) { + continue + } + const preferences = await control.findAll(control.ctx, view.class.ViewletPreference, { + config: attribute.name, + space: core.space.Workspace }) - res.push(tx) + for (const preference of preferences) { + const tx = control.txFactory.createTxUpdateDoc(preference._class, preference.space, preference._id, { + $pull: { config: attribute.name } + }) + result.push(tx) + } } - return res + return result } function checkTx (ptx: TxRemoveDoc, hierarchy: Hierarchy): boolean { diff --git a/server/account-service/src/index.ts b/server/account-service/src/index.ts index 9e0cb5f76c..3bfd42de8c 100644 --- a/server/account-service/src/index.ts +++ b/server/account-service/src/index.ts @@ -228,11 +228,7 @@ export function serveAccount (measureCtx: MeasureContext, brandings: BrandingMap host = new URL(origin).host } const branding = host !== undefined ? brandings[host] : null - const result = await measureCtx.with( - request.method, - {}, - async (ctx) => await method(ctx, db, branding, request, token) - ) + const result = await measureCtx.with(request.method, {}, (ctx) => method(ctx, db, branding, request, token)) ctx.body = result }) diff --git a/server/account/src/operations.ts b/server/account/src/operations.ts index 9177d06230..9b20451455 100644 --- a/server/account/src/operations.ts +++ b/server/account/src/operations.ts @@ -1087,10 +1087,8 @@ export async function workerHandshake ( return } - const workspacesCnt = await ctx.with( - 'count-workspaces-in-region', - {}, - async (ctx) => await countWorkspacesInRegion(db, region, version, Date.now() - 24 * 60 * 60 * 1000) + const workspacesCnt = await ctx.with('count-workspaces-in-region', {}, (ctx) => + countWorkspacesInRegion(db, region, version, Date.now() - 24 * 60 * 60 * 1000) ) await db.upgrade.insertOne({ @@ -1485,7 +1483,7 @@ export async function getWorkspaceInfo ( workspace: workspace.name } if (email !== systemAccountEmail && !guest) { - account = await ctx.with('get-account', {}, async () => await getAccount(db, email)) + account = await ctx.with('get-account', {}, () => getAccount(db, email)) if (account === null) { ctx.error('no account', { email, token }) throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {})) @@ -1520,9 +1518,7 @@ export async function getWorkspaceInfo ( throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {})) } if (ws.mode !== 'archived' && _updateLastVisit && (isAccount(account) || email === systemAccountEmail)) { - void ctx.with('update-last-visit', {}, async () => { - await updateLastVisit(db, ws, account as Account) - }) + void ctx.with('update-last-visit', {}, () => updateLastVisit(db, ws, account as Account)) } const clientWs: ClientWSInfoWithUpgrade = mapToClientWorkspace(ws) diff --git a/server/backup/src/backup.ts b/server/backup/src/backup.ts index 3c905afb3c..6959ded269 100644 --- a/server/backup/src/backup.ts +++ b/server/backup/src/backup.ts @@ -452,9 +452,7 @@ export async function cloneWorkspace ( ctx.info('clone domain...', { domain: c, workspace: targetWorkspaceId.name }) // We need to clean target connection before copying something. - await ctx.with('clean-domain', { domain: c }, async (ctx) => { - await cleanDomain(ctx, targetConnection, c) - }) + await ctx.with('clean-domain', { domain: c }, (ctx) => cleanDomain(ctx, targetConnection, c)) const changes: Snapshot = { added: new Map(), @@ -474,7 +472,7 @@ export async function cloneWorkspace ( await ctx.with('retrieve-domain-info', { domain: c }, async (ctx) => { while (true) { try { - const it = await ctx.with('load-chunk', {}, async () => await sourceConnection.loadChunk(c, idx)) + const it = await ctx.with('load-chunk', {}, () => sourceConnection.loadChunk(c, idx)) idx = it.idx let needRetrieve: Ref[] = [] @@ -510,9 +508,7 @@ export async function cloneWorkspace ( } catch (err: any) { ctx.error('failed to clone', { err, workspace: targetWorkspaceId.name }) if (idx !== undefined) { - await ctx.with('load-chunk', {}, async () => { - await sourceConnection.closeChunk(idx as number) - }) + await ctx.with('load-chunk', {}, () => sourceConnection.closeChunk(idx as number)) } // Try again idx = undefined @@ -527,7 +523,7 @@ export async function cloneWorkspace ( ctx.info('Retrieve chunk:', { count: needRetrieve.length }) let docs: Doc[] = [] try { - docs = await ctx.with('load-docs', {}, async (ctx) => await sourceConnection.loadDocs(c, needRetrieve)) + docs = await ctx.with('load-docs', {}, (ctx) => sourceConnection.loadDocs(c, needRetrieve)) if (clearTime) { docs = prepareClonedDocuments(docs, sourceConnection) } @@ -561,14 +557,7 @@ export async function cloneWorkspace ( } } await executor.waitProcessing() - await ctx.with( - 'upload-docs', - {}, - async (ctx) => { - await targetConnection.upload(c, docs) - }, - { length: docs.length } - ) + await ctx.with('upload-docs', {}, (ctx) => targetConnection.upload(c, docs), { length: docs.length }) await progress((100 / domains.length) * i + (100 / domains.length / processed) * domainProgress) } catch (err: any) { console.log(err) diff --git a/server/client/src/blob.ts b/server/client/src/blob.ts index afaa6d89c4..cfd1ad2caa 100644 --- a/server/client/src/blob.ts +++ b/server/client/src/blob.ts @@ -62,7 +62,7 @@ export class BlobClient { if (i === 4) { ctx.error('Failed to check file', { name, error: err }) } - await new Promise((resolve) => setTimeout(resolve, 500)) + await new Promise((resolve) => setTimeout(resolve, 10)) } } return false @@ -189,7 +189,7 @@ export class BlobClient { }) throw err } - await new Promise((resolve) => setTimeout(resolve, 1000)) + await new Promise((resolve) => setTimeout(resolve, 10)) // retry } } diff --git a/server/collaboration/src/utils/collaborative-doc.ts b/server/collaboration/src/utils/collaborative-doc.ts index 037279491a..f6d6f9cd29 100644 --- a/server/collaboration/src/utils/collaborative-doc.ts +++ b/server/collaboration/src/utils/collaborative-doc.ts @@ -42,14 +42,8 @@ async function loadCollaborativeDocVersion ( documentId: string, versionId: string ): Promise { - const yContent = await ctx.with('yDocFromStorage', { type: 'content' }, async (ctx) => { - return await yDocFromStorage( - ctx, - storageAdapter, - workspace, - documentId, - new YDoc({ guid: generateId(), gc: false }) - ) + const yContent = await ctx.with('yDocFromStorage', { type: 'content' }, (ctx) => { + return yDocFromStorage(ctx, storageAdapter, workspace, documentId, new YDoc({ guid: generateId(), gc: false })) }) // the document does not exist @@ -62,8 +56,8 @@ async function loadCollaborativeDocVersion ( } const historyDocumentId = collaborativeHistoryDocId(documentId) - const yHistory = await ctx.with('yDocFromStorage', { type: 'history' }, async (ctx) => { - return await yDocFromStorage(ctx, storageAdapter, workspace, historyDocumentId, new YDoc({ guid: generateId() })) + const yHistory = await ctx.with('yDocFromStorage', { type: 'history' }, (ctx) => { + return yDocFromStorage(ctx, storageAdapter, workspace, historyDocumentId, new YDoc({ guid: generateId() })) }) // the history document does not exist @@ -122,9 +116,7 @@ export async function saveCollaborativeDocVersion ( ): Promise { await ctx.with('saveCollaborativeDoc', {}, async (ctx) => { if (versionId === 'HEAD') { - await ctx.with('yDocToStorage', {}, async () => { - await yDocToStorage(ctx, storageAdapter, workspace, documentId, ydoc) - }) + await ctx.with('yDocToStorage', {}, () => yDocToStorage(ctx, storageAdapter, workspace, documentId, ydoc)) } else { console.warn('Cannot save non HEAD document version', documentId, versionId) } @@ -149,9 +141,7 @@ export async function removeCollaborativeDoc ( } } if (toRemove.length > 0) { - await ctx.with('remove', {}, async () => { - await storageAdapter.remove(ctx, workspace, toRemove) - }) + await ctx.with('remove', {}, () => storageAdapter.remove(ctx, workspace, toRemove)) } }) } diff --git a/server/collaborator/src/extensions/storage.ts b/server/collaborator/src/extensions/storage.ts index 3a8cf2ba43..db49e51148 100644 --- a/server/collaborator/src/extensions/storage.ts +++ b/server/collaborator/src/extensions/storage.ts @@ -129,8 +129,8 @@ export class StorageExtension implements Extension { const { ctx, adapter } = this.configuration try { - return await ctx.with('load-document', {}, async (ctx) => { - return await adapter.loadDocument(ctx, documentName as DocumentId, context) + return await ctx.with('load-document', {}, (ctx) => { + return adapter.loadDocument(ctx, documentName as DocumentId, context) }) } catch (err) { ctx.error('failed to load document', { documentName, error: err }) @@ -145,12 +145,12 @@ export class StorageExtension implements Extension { const prevMarkup = this.markups.get(documentName) ?? {} const currMarkup = this.configuration.transformer.fromYdoc(document) - await ctx.with('save-document', {}, async (ctx) => { - await adapter.saveDocument(ctx, documentName as DocumentId, document, context, { + await ctx.with('save-document', {}, (ctx) => + adapter.saveDocument(ctx, documentName as DocumentId, document, context, { prev: prevMarkup, curr: currMarkup }) - }) + ) this.markups.set(documentName, currMarkup) } catch (err) { diff --git a/server/collaborator/src/rpc/methods/getContent.ts b/server/collaborator/src/rpc/methods/getContent.ts index bf6b87836b..90aeb92cdf 100644 --- a/server/collaborator/src/rpc/methods/getContent.ts +++ b/server/collaborator/src/rpc/methods/getContent.ts @@ -27,8 +27,8 @@ export async function getContent ( ): Promise { const { hocuspocus, transformer } = params - const connection = await ctx.with('connect', {}, async () => { - return await hocuspocus.openDirectConnection(documentId, context) + const connection = await ctx.with('connect', {}, () => { + return hocuspocus.openDirectConnection(documentId, context) }) try { diff --git a/server/collaborator/src/rpc/methods/updateContent.ts b/server/collaborator/src/rpc/methods/updateContent.ts index 6544a22474..1334f0265c 100644 --- a/server/collaborator/src/rpc/methods/updateContent.ts +++ b/server/collaborator/src/rpc/methods/updateContent.ts @@ -13,8 +13,8 @@ // limitations under the License. // -import { MeasureContext } from '@hcengineering/core' import { type UpdateContentRequest, type UpdateContentResponse } from '@hcengineering/collaborator-client' +import { MeasureContext } from '@hcengineering/core' import { applyUpdate, encodeStateAsUpdate } from 'yjs' import { Context } from '../../context' import { RpcMethodParams } from '../rpc' @@ -29,7 +29,7 @@ export async function updateContent ( const { content } = payload const { hocuspocus, transformer } = params - const updates = await ctx.with('transform', {}, () => { + const updates = ctx.withSync('transform', {}, () => { const updates: Record = {} Object.entries(content).forEach(([field, markup]) => { @@ -40,13 +40,13 @@ export async function updateContent ( return updates }) - const connection = await ctx.with('connect', {}, async () => { - return await hocuspocus.openDirectConnection(documentId, context) + const connection = await ctx.with('connect', {}, () => { + return hocuspocus.openDirectConnection(documentId, context) }) try { - await ctx.with('update', {}, async () => { - await connection.transact((document) => { + await ctx.with('update', {}, () => + connection.transact((document) => { document.transact(() => { Object.entries(updates).forEach(([field, update]) => { const fragment = document.getXmlFragment(field) @@ -55,7 +55,7 @@ export async function updateContent ( }) }, connection) }) - }) + ) } finally { await connection.disconnect() } diff --git a/server/collaborator/src/server.ts b/server/collaborator/src/server.ts index 70ce89e635..8e7422ebc0 100644 --- a/server/collaborator/src/server.ts +++ b/server/collaborator/src/server.ts @@ -169,8 +169,8 @@ export async function start (ctx: MeasureContext, config: Config, storageAdapter rpcCtx.info('rpc', { method: request.method, connectionId: context.connectionId, mode: token.extra?.mode ?? '' }) await rpcCtx.with('/rpc', { method: request.method }, async (ctx) => { try { - const response: RpcResponse = await rpcCtx.with(request.method, {}, async (ctx) => { - return await method(ctx, context, documentId, request.payload, { hocuspocus, storageAdapter, transformer }) + const response: RpcResponse = await rpcCtx.with(request.method, {}, (ctx) => { + return method(ctx, context, documentId, request.payload, { hocuspocus, storageAdapter, transformer }) }) res.status(200).send(response) } catch (err: any) { diff --git a/server/collaborator/src/storage/platform.ts b/server/collaborator/src/storage/platform.ts index bdc417cca5..3179ae5e5d 100644 --- a/server/collaborator/src/storage/platform.ts +++ b/server/collaborator/src/storage/platform.ts @@ -33,8 +33,8 @@ import { Doc as YDoc } from 'yjs' import { Context } from '../context' -import { CollabStorageAdapter } from './adapter' import { areEqualMarkups } from '@hcengineering/text' +import { CollabStorageAdapter } from './adapter' export class PlatformStorageAdapter implements CollabStorageAdapter { constructor (private readonly storage: StorageAdapter) {} @@ -89,9 +89,7 @@ export class PlatformStorageAdapter implements CollabStorageAdapter { ): Promise { const { clientFactory } = context - const client = await ctx.with('connect', {}, async () => { - return await clientFactory() - }) + const client = await ctx.with('connect', {}, () => clientFactory()) try { try { @@ -107,9 +105,9 @@ export class PlatformStorageAdapter implements CollabStorageAdapter { const { platformDocumentId } = context if (platformDocumentId !== undefined) { ctx.info('save document content to platform', { documentId, platformDocumentId }) - await ctx.with('save-to-platform', {}, async (ctx) => { - await this.saveDocumentToPlatform(ctx, client, documentId, platformDocumentId, markup) - }) + await ctx.with('save-to-platform', {}, (ctx) => + this.saveDocumentToPlatform(ctx, client, documentId, platformDocumentId, markup) + ) } } finally { await client.close() @@ -123,11 +121,11 @@ export class PlatformStorageAdapter implements CollabStorageAdapter { ): Promise { const { collaborativeDoc } = parseDocumentId(documentId) - return await ctx.with('load-document', {}, async (ctx) => { - return await withRetry(ctx, 5, async () => { + return await ctx.with('load-document', {}, (ctx) => + withRetry(ctx, 5, async () => { return await loadCollaborativeDoc(ctx, this.storage, context.workspaceId, collaborativeDoc) }) - }) + ) } async saveDocumentToStorage ( @@ -138,11 +136,11 @@ export class PlatformStorageAdapter implements CollabStorageAdapter { ): Promise { const { collaborativeDoc } = parseDocumentId(documentId) - await ctx.with('save-document', {}, async (ctx) => { - await withRetry(ctx, 5, async () => { + await ctx.with('save-document', {}, (ctx) => + withRetry(ctx, 5, async () => { await saveCollaborativeDoc(ctx, this.storage, context.workspaceId, collaborativeDoc, document) }) - }) + ) } async saveDocumentToPlatform ( @@ -171,8 +169,8 @@ export class PlatformStorageAdapter implements CollabStorageAdapter { return } - const current = await ctx.with('query', {}, async () => { - return await client.findOne(objectClass, { _id: objectId }) + const current = await ctx.with('query', {}, () => { + return client.findOne(objectClass, { _id: objectId }) }) if (current === undefined) { @@ -189,11 +187,9 @@ export class PlatformStorageAdapter implements CollabStorageAdapter { const collaborativeDoc = (current as any)[objectAttr] as CollaborativeDoc const newCollaborativeDoc = collaborativeDocWithLastVersion(collaborativeDoc, `${Date.now()}`) - await ctx.with('update', {}, async () => { - await client.diffUpdate(current, { [objectAttr]: newCollaborativeDoc }) - }) + await ctx.with('update', {}, () => client.diffUpdate(current, { [objectAttr]: newCollaborativeDoc })) - await ctx.with('activity', {}, async () => { + await ctx.with('activity', {}, () => { const data: AttachedData = { objectId, objectClass, @@ -208,7 +204,7 @@ export class PlatformStorageAdapter implements CollabStorageAdapter { isMixin: hierarchy.isMixin(objectClass) } } - await client.addCollection( + return client.addCollection( activity.class.DocUpdateMessage, current.space, current._id, diff --git a/server/core/src/benchmark/index.ts b/server/core/src/benchmark/index.ts index 52560a198b..05d0f1d803 100644 --- a/server/core/src/benchmark/index.ts +++ b/server/core/src/benchmark/index.ts @@ -90,7 +90,7 @@ class BenchmarkDbAdapter extends DummyDbAdapter { return toFindResult(result as T[]) } - async tx (ctx: MeasureContext, ...tx: Tx[]): Promise { + tx (ctx: MeasureContext, ...tx: Tx[]): Promise { if (benchData === '') { benchData = genData(1024 * 1024) } @@ -102,16 +102,16 @@ class BenchmarkDbAdapter extends DummyDbAdapter { if (request?.size != null) { const dataSize = typeof request.size === 'number' ? request.size : request.size.from + Math.random() * request.size.to - return [ + return Promise.resolve([ { response: benchData.slice(0, dataSize) } - ] + ]) } } } - return [{}] + return Promise.resolve([{}]) } } /** diff --git a/server/core/src/dbAdapterManager.ts b/server/core/src/dbAdapterManager.ts index 1243b184ca..d6bba2e5ca 100644 --- a/server/core/src/dbAdapterManager.ts +++ b/server/core/src/dbAdapterManager.ts @@ -99,8 +99,8 @@ export class DbAdapterManagerImpl implements DBAdapterManager { } } - async initAdapters (ctx: MeasureContext): Promise { - await ctx.with('init-adapters', {}, async (ctx) => { + initAdapters (ctx: MeasureContext): Promise { + return ctx.with('init-adapters', {}, async (ctx) => { for (const [key, adapter] of this.adapters) { // already initialized if (key !== this.conf.domains[DOMAIN_TX] && adapter.init !== undefined) { diff --git a/server/core/src/mem.ts b/server/core/src/mem.ts index 91ff22dd4d..80d53218d4 100644 --- a/server/core/src/mem.ts +++ b/server/core/src/mem.ts @@ -133,20 +133,20 @@ class InMemoryAdapter extends DummyDbAdapter implements DbAdapter { this.modeldb = new ModelDb(hierarchy) } - async findAll( + findAll( ctx: MeasureContext, _class: Ref>, query: DocumentQuery, options?: FindOptions ): Promise> { - return await this.modeldb.findAll(_class, query, options) + return ctx.withSync('inmem-find', {}, () => this.modeldb.findAll(_class, query, options)) } - async load (ctx: MeasureContext, domain: Domain, docs: Ref[]): Promise { - return await this.modeldb.findAll(core.class.Doc, { _id: { $in: docs } }) + load (ctx: MeasureContext, domain: Domain, docs: Ref[]): Promise { + return this.modeldb.findAll(core.class.Doc, { _id: { $in: docs } }) } - async tx (ctx: MeasureContext, ...tx: Tx[]): Promise { + tx (ctx: MeasureContext, ...tx: Tx[]): Promise { // Filter transactions with broadcast only flags const ftx = tx.filter((it) => { if (TxProcessor.isExtendsCUD(it._class)) { @@ -161,9 +161,9 @@ class InMemoryAdapter extends DummyDbAdapter implements DbAdapter { return true }) if (ftx.length === 0) { - return [] + return Promise.resolve([]) } - return await this.modeldb.tx(...ftx) + return this.modeldb.tx(...ftx) } } diff --git a/server/core/src/pipeline.ts b/server/core/src/pipeline.ts index 5b286b445b..1b600a2184 100644 --- a/server/core/src/pipeline.ts +++ b/server/core/src/pipeline.ts @@ -92,37 +92,37 @@ class PipelineImpl implements Pipeline { return current } - async findAll( + findAll( ctx: MeasureContext, _class: Ref>, query: DocumentQuery, options?: FindOptions ): Promise> { - return this.head !== undefined ? await this.head.findAll(ctx, _class, query, options) : toFindResult([]) + return this.head !== undefined ? this.head.findAll(ctx, _class, query, options) : Promise.resolve(toFindResult([])) } - async loadModel (ctx: MeasureContext, lastModelTx: Timestamp, hash?: string): Promise { - return this.head !== undefined ? await this.head.loadModel(ctx, lastModelTx, hash) : [] + loadModel (ctx: MeasureContext, lastModelTx: Timestamp, hash?: string): Promise { + return this.head !== undefined ? this.head.loadModel(ctx, lastModelTx, hash) : Promise.resolve([]) } - async groupBy( + groupBy( ctx: MeasureContext, domain: Domain, field: string, query?: DocumentQuery

): Promise> { - return this.head !== undefined ? await this.head.groupBy(ctx, domain, field, query) : new Map() + return this.head !== undefined ? this.head.groupBy(ctx, domain, field, query) : Promise.resolve(new Map()) } - async searchFulltext (ctx: MeasureContext, query: SearchQuery, options: SearchOptions): Promise { - return this.head !== undefined ? await this.head.searchFulltext(ctx, query, options) : { docs: [] } + searchFulltext (ctx: MeasureContext, query: SearchQuery, options: SearchOptions): Promise { + return this.head !== undefined ? this.head.searchFulltext(ctx, query, options) : Promise.resolve({ docs: [] }) } - async tx (ctx: MeasureContext, tx: Tx[]): Promise { + tx (ctx: MeasureContext, tx: Tx[]): Promise { if (this.head !== undefined) { - return await this.head.tx(ctx, tx) + return this.head.tx(ctx, tx) } - return {} + return Promise.resolve({}) } handleBroadcast (ctx: MeasureContext): Promise { diff --git a/server/core/src/stats.ts b/server/core/src/stats.ts index c46d65f982..beb505d9ee 100644 --- a/server/core/src/stats.ts +++ b/server/core/src/stats.ts @@ -129,7 +129,6 @@ export function initStatisticsContext ( const statData = JSON.stringify(data) - metricsContext.info('send stats:', { size: statData.length }) void fetch( concatLink(statsUrl, '/api/v1/statistics') + `/?token=${encodeURIComponent(token)}&name=${serviceId}`, { diff --git a/server/core/src/storage.ts b/server/core/src/storage.ts index 8d436aa733..e16e0202fd 100644 --- a/server/core/src/storage.ts +++ b/server/core/src/storage.ts @@ -41,7 +41,7 @@ export class BackupClientOps { idIndex = 0 chunkInfo = new Map() - async loadChunk ( + loadChunk ( ctx: MeasureContext, domain: Domain, idx?: number, @@ -51,7 +51,7 @@ export class BackupClientOps { docs: DocInfo[] finished: boolean }> { - return await ctx.with('load-chunk', { domain }, async (ctx) => { + return ctx.with('load-chunk', {}, async (ctx) => { idx = idx ?? this.idIndex++ let chunk: ChunkInfo | undefined = this.chunkInfo.get(idx) if (chunk !== undefined) { @@ -90,8 +90,8 @@ export class BackupClientOps { }) } - async closeChunk (ctx: MeasureContext, idx: number): Promise { - await ctx.with('close-chunk', {}, async () => { + closeChunk (ctx: MeasureContext, idx: number): Promise { + return ctx.with('close-chunk', {}, async () => { const chunk = this.chunkInfo.get(idx) this.chunkInfo.delete(idx) if (chunk != null) { diff --git a/server/core/src/triggers.ts b/server/core/src/triggers.ts index 0266a927c8..1b707323c1 100644 --- a/server/core/src/triggers.ts +++ b/server/core/src/triggers.ts @@ -17,6 +17,7 @@ import core, { TxFactory, TxProcessor, + generateId, groupByArray, matchQuery, type Class, @@ -41,8 +42,6 @@ import serverCore from './plugin' interface TriggerRecord { query?: DocumentQuery trigger: { op: TriggerFunc | Promise, resource: Resource, isAsync: boolean } - - arrays: boolean } /** * @public @@ -67,12 +66,12 @@ export class Triggers { const isAsync = t.isAsync === true this.triggers.push({ query: match, - trigger: { op: func, resource: trigger, isAsync }, - arrays: t.arrays === true + trigger: { op: func, resource: trigger, isAsync } }) } - async tx (txes: Tx[]): Promise { + tresolve = Promise.resolve() + tx (txes: Tx[]): Promise { for (const tx of txes) { if (tx._class === core.class.TxCreateDoc) { const createTx = tx as TxCreateDoc @@ -82,13 +81,14 @@ export class Triggers { } } } + return this.tresolve } async applyTrigger ( ctx: MeasureContext, ctrl: Omit, matches: Tx[], - { trigger, arrays }: TriggerRecord + { trigger }: TriggerRecord ): Promise { const result: Tx[] = [] const apply: Tx[] = [] @@ -98,29 +98,26 @@ export class Triggers { ...ctrl, ctx, txFactory: null as any, // Will be set later - apply: async (ctx, tx, needResult) => { + apply: (ctx, tx, needResult) => { if (needResult !== true) { apply.push(...tx) } ctrl.txes.push(...tx) // We need to put them so other triggers could check if similar operation is already performed. - return await ctrl.apply(ctx, tx, needResult) + return ctrl.apply(ctx, tx, needResult) } } if (trigger.op instanceof Promise) { trigger.op = await trigger.op } for (const [k, v] of group.entries()) { - const m = arrays ? [v] : v tctrl.txFactory = new TxFactory(k, true) - for (const tx of m) { - try { - const tresult = await trigger.op(tx, tctrl) - result.push(...tresult) - ctrl.txes.push(...tresult) - } catch (err: any) { - ctx.error('failed to process trigger', { trigger: trigger.resource, tx, err }) - Analytics.handleError(err) - } + try { + const tresult = await trigger.op(v, tctrl) + result.push(...tresult) + ctrl.txes.push(...tresult) + } catch (err: any) { + ctx.error('failed to process trigger', { trigger: trigger.resource, err }) + Analytics.handleError(err) } } return result.concat(apply) @@ -133,7 +130,7 @@ export class Triggers { mode: 'sync' | 'async' ): Promise { const result: Tx[] = [] - for (const { query, trigger, arrays } of this.triggers) { + for (const { query, trigger } of this.triggers) { if ((trigger.isAsync ? 'async' : 'sync') !== mode) { continue } @@ -148,10 +145,16 @@ export class Triggers { trigger.resource, {}, async (ctx) => { - const tresult = await this.applyTrigger(ctx, ctrl, matches, { trigger, arrays }) + if (mode === 'async') { + ctx.id = generateId() + } + const tresult = await this.applyTrigger(ctx, ctrl, matches, { trigger }) result.push(...tresult) + if (ctx.onEnd !== undefined && mode === 'async') { + await ctx.onEnd(ctx) + } }, - { count: matches.length, arrays } + { count: matches.length } ) } } diff --git a/server/core/src/types.ts b/server/core/src/types.ts index f3697430b1..d6b4b2461f 100644 --- a/server/core/src/types.ts +++ b/server/core/src/types.ts @@ -260,7 +260,7 @@ export interface TriggerControl { /** * @public */ -export type TriggerFunc = (tx: Tx | Tx[], ctrl: TriggerControl) => Promise +export type TriggerFunc = (tx: Tx[], ctrl: TriggerControl) => Promise /** * @public @@ -273,9 +273,6 @@ export interface Trigger extends Doc { // We should match transaction txMatch?: DocumentQuery - - // If set trigger will handle Tx[] instead of Tx - arrays?: boolean } /** @@ -572,6 +569,11 @@ export function disableLogging (): void { LOGGING_ENABLED = false } +interface TickHandler { + ticks: number + operation: () => void +} + /** * @public */ @@ -581,6 +583,9 @@ export interface Workspace { token: string // Account workspace update token. pipeline: Promise tickHash: number + + tickHandlers: Map + sessions: Map upgrade: boolean diff --git a/server/datalake/src/client.ts b/server/datalake/src/client.ts index 2330454c90..cadaeea1db 100644 --- a/server/datalake/src/client.ts +++ b/server/datalake/src/client.ts @@ -161,13 +161,13 @@ export class Client { try { if (size === undefined || size < 64 * 1024 * 1024) { - await ctx.with('direct-upload', {}, async (ctx) => { - await this.uploadWithFormData(ctx, workspace, objectName, stream, metadata) - }) + await ctx.with('direct-upload', {}, (ctx) => + this.uploadWithFormData(ctx, workspace, objectName, stream, metadata) + ) } else { - await ctx.with('signed-url-upload', {}, async (ctx) => { - await this.uploadWithSignedURL(ctx, workspace, objectName, stream, metadata) - }) + await ctx.with('signed-url-upload', {}, (ctx) => + this.uploadWithSignedURL(ctx, workspace, objectName, stream, metadata) + ) } } catch (err) { console.error('failed to put object', { workspace, objectName, err }) diff --git a/server/datalake/src/index.ts b/server/datalake/src/index.ts index 5cec66083e..7c2e88c884 100644 --- a/server/datalake/src/index.ts +++ b/server/datalake/src/index.ts @@ -126,11 +126,9 @@ export class DatalakeService implements StorageAdapter { size } - await ctx.with('put', {}, async (ctx) => { - await withRetry(ctx, 5, async () => { - await this.client.putObject(ctx, workspaceId, objectName, stream, metadata, size) - }) - }) + await ctx.with('put', {}, (ctx) => + withRetry(ctx, 5, () => this.client.putObject(ctx, workspaceId, objectName, stream, metadata, size)) + ) return { etag: '', diff --git a/server/elastic/src/adapter.ts b/server/elastic/src/adapter.ts index c49fba0871..2598a9c9db 100644 --- a/server/elastic/src/adapter.ts +++ b/server/elastic/src/adapter.ts @@ -62,7 +62,7 @@ class ElasticAdapter implements FullTextAdapter { async initMapping (ctx: MeasureContext): Promise { const indexName = this.indexName try { - const existingVersions = await ctx.with('get-indexes', {}, () => + const existingVersions = await ctx.withSync('get-indexes', {}, () => this.client.indices.get({ index: [`${this.indexBaseName}_*`] }) diff --git a/server/front/src/index.ts b/server/front/src/index.ts index 13895a1340..70ebeaddbe 100644 --- a/server/front/src/index.ts +++ b/server/front/src/index.ts @@ -49,7 +49,7 @@ async function storageUpload ( const resp = await ctx.with( 'storage upload', { workspace: workspace.name }, - async (ctx) => await storageAdapter.put(ctx, workspace, uuid, data, file.mimetype, file.size), + (ctx) => storageAdapter.put(ctx, workspace, uuid, data, file.mimetype, file.size), { file: file.name, contentType: file.mimetype } ) @@ -104,7 +104,7 @@ async function getFileRange ( const dataStream = await ctx.with( 'partial', {}, - async (ctx) => await client.partial(ctx, workspace, stat._id, start, end - start + 1), + (ctx) => client.partial(ctx, workspace, stat._id, start, end - start + 1), {} ) res.writeHead(206, { @@ -199,7 +199,7 @@ async function getFile ( { contentType: stat.contentType }, async (ctx) => { try { - const dataStream = await ctx.with('readable', {}, async (ctx) => await client.get(ctx, workspace, stat._id)) + const dataStream = await ctx.with('readable', {}, (ctx) => client.get(ctx, workspace, stat._id)) res.writeHead(200, { 'Content-Type': stat.contentType, Etag: stat.etag, @@ -400,10 +400,8 @@ export function start ( return } - let blobInfo = await ctx.with( - 'stat', - { workspace: payload.workspace.name }, - async (ctx) => await config.storageAdapter.stat(ctx, payload.workspace, uuid) + let blobInfo = await ctx.with('stat', { workspace: payload.workspace.name }, (ctx) => + config.storageAdapter.stat(ctx, payload.workspace, uuid) ) if (blobInfo === undefined) { @@ -431,28 +429,23 @@ export function start ( const size = req.query.size !== undefined ? parseInt(req.query.size as string) : undefined const accept = req.headers.accept if (accept !== undefined && isImage && blobInfo.contentType !== 'image/gif' && size !== undefined) { - blobInfo = await ctx.with( - 'resize', - {}, - async (ctx) => - await getGeneratePreview(ctx, blobInfo as PlatformBlob, size, uuid, config, payload, accept, () => - join(tempFileDir, `${++temoFileIndex}`) - ) + blobInfo = await ctx.with('resize', {}, (ctx) => + getGeneratePreview(ctx, blobInfo as PlatformBlob, size, uuid, config, payload, accept, () => + join(tempFileDir, `${++temoFileIndex}`) + ) ) } const range = req.headers.range if (range !== undefined) { - await ctx.with('file-range', { workspace: payload.workspace.name }, async (ctx) => { - await getFileRange(ctx, blobInfo as PlatformBlob, range, config.storageAdapter, payload.workspace, res) - }) + await ctx.with('file-range', { workspace: payload.workspace.name }, (ctx) => + getFileRange(ctx, blobInfo as PlatformBlob, range, config.storageAdapter, payload.workspace, res) + ) } else { await ctx.with( 'file', { workspace: payload.workspace.name }, - async (ctx) => { - await getFile(ctx, blobInfo as PlatformBlob, config.storageAdapter, payload.workspace, req, res) - }, + (ctx) => getFile(ctx, blobInfo as PlatformBlob, config.storageAdapter, payload.workspace, req, res), { uuid } ) } @@ -867,7 +860,7 @@ async function getGeneratePreview ( const outFile = tempFile() files.push(outFile) - const dataBuff = await ctx.with('resize', { contentType }, async () => await pipeline.toFile(outFile)) + const dataBuff = await ctx.with('resize', { contentType }, () => pipeline.toFile(outFile)) pipeline.destroy() // Add support of avif as well. diff --git a/server/indexer/src/indexer/indexer.ts b/server/indexer/src/indexer/indexer.ts index 3d74b393ce..cb576d29f3 100644 --- a/server/indexer/src/indexer/indexer.ts +++ b/server/indexer/src/indexer/indexer.ts @@ -309,7 +309,7 @@ export class FullTextIndexPipeline implements FullTextPipeline { const { classUpdate: _classes, processed } = await this.metrics.with( 'processIndex', { workspace: this.workspace.name }, - async (ctx) => await this.processIndex(ctx, indexing) + (ctx) => this.processIndex(ctx, indexing) ) // Also update doc index state queries. @@ -445,8 +445,8 @@ export class FullTextIndexPipeline implements FullTextPipeline { let result: FindResult | WithLookup[] = await ctx.with( 'get-indexable', {}, - async (ctx) => { - return await this.storage.findAll(ctx, core.class.DocIndexState, q, { + (ctx) => { + return this.storage.findAll(ctx, core.class.DocIndexState, q, { limit: globalIndexer.processingSize, skipClass: true, skipSpace: true, @@ -589,9 +589,7 @@ export class FullTextIndexPipeline implements FullTextPipeline { await pushQueue.exec(async () => { try { try { - await ctx.with('push-elastic', {}, async () => { - await this.fulltextAdapter.updateMany(ctx, this.workspace, docs) - }) + await ctx.with('push-elastic', {}, () => this.fulltextAdapter.updateMany(ctx, this.workspace, docs)) } catch (err: any) { Analytics.handleError(err) // Try to push one by one diff --git a/server/middleware/src/contextName.ts b/server/middleware/src/contextName.ts index 2c9450c545..4e712462df 100644 --- a/server/middleware/src/contextName.ts +++ b/server/middleware/src/contextName.ts @@ -54,9 +54,9 @@ export class ContextNameMiddleware extends BaseMiddleware implements Middleware const result = await ctx.with( measureName !== undefined ? `📶 ${measureName}` : 'client-tx', { _class: tx?._class }, - async (ctx) => { + (ctx) => { ;({ opLogMetrics, op } = registerOperationLog(ctx)) - return await this.provideTx(ctx as MeasureContext, txes) + return this.provideTx(ctx as MeasureContext, txes) } ) updateOperationLog(opLogMetrics, op) diff --git a/server/middleware/src/fulltext.ts b/server/middleware/src/fulltext.ts index ee8a8cebb0..0ee86b7821 100644 --- a/server/middleware/src/fulltext.ts +++ b/server/middleware/src/fulltext.ts @@ -16,6 +16,7 @@ import { Analytics } from '@hcengineering/analytics' import core, { docKey, + DOMAIN_DOC_INDEX_STATE, isFullTextAttribute, isIndexedAttribute, toFindResult, @@ -80,8 +81,11 @@ export class FullTextMiddleware extends BaseMiddleware implements Middleware { } async handleBroadcast (ctx: MeasureContext): Promise { - if (ctx.contextData.needWarmupFulltext === true) { - void this.sendWarmup() + if (ctx.contextData.fulltextUpdates !== undefined && ctx.contextData.fulltextUpdates.size > 0) { + const toUpdate = Array.from(ctx.contextData.fulltextUpdates.values()) + ctx.contextData.fulltextUpdates.clear() + await this.context.lowLevelStorage?.upload(ctx, DOMAIN_DOC_INDEX_STATE, toUpdate) + this.sendWarmup() } await super.handleBroadcast(ctx) } @@ -95,15 +99,15 @@ export class FullTextMiddleware extends BaseMiddleware implements Middleware { ) // Send one warumup so reindex will - void this.sendWarmup() + this.sendWarmup() } - async sendWarmup (): Promise { + sendWarmup (): void { if (!this.warmupTriggered) { this.warmupTriggered = true setTimeout(() => { this.warmupTriggered = false - void this._sendWarmup() + void this._sendWarmup().catch(() => {}) }, 25) } } diff --git a/server/middleware/src/liveQuery.ts b/server/middleware/src/liveQuery.ts index 4b6d91717e..8d570a3944 100644 --- a/server/middleware/src/liveQuery.ts +++ b/server/middleware/src/liveQuery.ts @@ -48,7 +48,7 @@ export class LiveQueryMiddleware extends BaseMiddleware implements Middleware { getModel (): ModelDb { return modelDb }, - close: async () => {}, + close: () => Promise.resolve(), findAll: async (_class, query, options) => { const _ctx: MeasureContext = (options as ServerFindOptions)?.ctx ?? metrics delete (options as ServerFindOptions)?.ctx @@ -73,8 +73,8 @@ export class LiveQueryMiddleware extends BaseMiddleware implements Middleware { results.total )[0] }, - tx: async (tx) => { - return {} + tx: (tx) => { + return Promise.resolve({}) }, searchFulltext: async (query: SearchQuery, options: SearchOptions) => { // Cast client doesn't support fulltext search diff --git a/server/middleware/src/lowLevel.ts b/server/middleware/src/lowLevel.ts index 9d4d8b8004..f0254cbfa8 100644 --- a/server/middleware/src/lowLevel.ts +++ b/server/middleware/src/lowLevel.ts @@ -55,8 +55,8 @@ export class LowLevelMiddleware extends BaseMiddleware implements Middleware { return adapterManager.getAdapter(domain, true).upload(ctx, domain, docs) }, - async clean (ctx: MeasureContext, domain: Domain, docs: Ref[]): Promise { - await adapterManager.getAdapter(domain, true).clean(ctx, domain, docs) + clean (ctx: MeasureContext, domain: Domain, docs: Ref[]): Promise { + return adapterManager.getAdapter(domain, true).clean(ctx, domain, docs) }, groupBy( ctx: MeasureContext, diff --git a/server/middleware/src/private.ts b/server/middleware/src/private.ts index c1fc68b8a0..cc1707348d 100644 --- a/server/middleware/src/private.ts +++ b/server/middleware/src/private.ts @@ -122,7 +122,7 @@ export class PrivateMiddleware extends BaseMiddleware implements Middleware { if (options?.lookup !== undefined) { for (const object of findResult) { if (object.$lookup !== undefined) { - await this.filterLookup(ctx, object.$lookup) + this.filterLookup(ctx, object.$lookup) } } } @@ -136,7 +136,7 @@ export class PrivateMiddleware extends BaseMiddleware implements Middleware { return doc.createdBy === account || account === core.account.System } - async filterLookup(ctx: MeasureContext, lookup: LookupData): Promise { + filterLookup(ctx: MeasureContext, lookup: LookupData): void { for (const key in lookup) { const val = lookup[key] if (Array.isArray(val)) { diff --git a/server/middleware/src/triggers.ts b/server/middleware/src/triggers.ts index de1f16ab92..90344da64a 100644 --- a/server/middleware/src/triggers.ts +++ b/server/middleware/src/triggers.ts @@ -51,7 +51,6 @@ 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 @@ -107,7 +106,7 @@ export class TriggersMiddleware extends BaseMiddleware implements Middleware { } private async processDerived (ctx: MeasureContext, txes: Tx[]): Promise { - const _findAll: SessionFindAll = async (ctx, _class, query, options) => { + const findAll: SessionFindAll = async (ctx, _class, query, options) => { const _ctx: MeasureContext = (options as ServerFindOptions)?.ctx ?? ctx delete (options as ServerFindOptions)?.ctx if (_ctx.contextData !== undefined) { @@ -123,11 +122,6 @@ 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)) @@ -267,14 +261,14 @@ export class TriggersMiddleware extends BaseMiddleware implements Middleware { } } - private async getCollectionUpdateTx( + private getCollectionUpdateTx( _id: Ref, _class: Ref>, modifiedBy: Ref, modifiedOn: number, attachedTo: Pick, update: DocumentUpdate - ): Promise { + ): Tx { const txFactory = new TxFactory(modifiedBy, true) const baseClass = this.context.hierarchy.getBaseClass(_class) if (baseClass !== _class) { @@ -363,7 +357,7 @@ export class TriggersMiddleware extends BaseMiddleware implements Middleware { const attr = this.context.hierarchy.findAttribute(oldAttachedTo._class, colTx.collection) if (attr !== undefined) { - oldTx = await this.getCollectionUpdateTx(_id, _class, tx.modifiedBy, colTx.modifiedOn, oldAttachedTo, { + oldTx = this.getCollectionUpdateTx(_id, _class, tx.modifiedBy, colTx.modifiedOn, oldAttachedTo, { $inc: { [colTx.collection]: -1 } }) } @@ -375,7 +369,7 @@ export class TriggersMiddleware extends BaseMiddleware implements Middleware { let newTx: Tx | null = null const newAttr = this.context.hierarchy.findAttribute(newAttachedToClass, newAttachedToCollection) if (newAttachedTo !== undefined && newAttr !== undefined) { - newTx = await this.getCollectionUpdateTx( + newTx = this.getCollectionUpdateTx( newAttachedTo._id, newAttachedTo._class, tx.modifiedBy, @@ -419,7 +413,7 @@ export class TriggersMiddleware extends BaseMiddleware implements Middleware { const attachedTo = (await findAll(ctx, _class, { _id }, { limit: 1 }))[0] if (attachedTo !== undefined) { result.push( - await this.getCollectionUpdateTx( + this.getCollectionUpdateTx( _id, _class, tx.modifiedBy, @@ -496,8 +490,8 @@ export class TriggersMiddleware extends BaseMiddleware implements Middleware { serverCore.mixin.ObjectDDParticipant ) const collector = await getResource(removeParticipand.collectDocs) - const docs = await collector(object, this.context.hierarchy, async (_class, query, options) => { - return await findAll(ctx, _class, query, options) + const docs = await collector(object, this.context.hierarchy, (_class, query, options) => { + return findAll(ctx, _class, query, options) }) for (const d of docs) { result.push(...this.deleteObject(ctx, d, ctx.contextData.removedMap)) diff --git a/server/middleware/src/txPush.ts b/server/middleware/src/txPush.ts index f34a15ce97..645ceec041 100644 --- a/server/middleware/src/txPush.ts +++ b/server/middleware/src/txPush.ts @@ -62,7 +62,7 @@ export class TxMiddleware extends BaseMiddleware implements Middleware { txPromise = ctx.with( 'domain-tx', {}, - async (ctx) => await this.adapterManager.getAdapter(DOMAIN_TX, true).tx(ctx, ...txToStore), + (ctx) => this.adapterManager.getAdapter(DOMAIN_TX, true).tx(ctx, ...txToStore), { count: txToStore.length, txes: Array.from(new Set(txToStore.map((it) => it._class))) @@ -70,7 +70,7 @@ export class TxMiddleware extends BaseMiddleware implements Middleware { ) } if (txPromise !== undefined) { - return (await Promise.all([txPromise, this.provideTx(ctx, txes)]))[1] + await txPromise } return await this.provideTx(ctx, txes) } diff --git a/server/mongo/src/__tests__/storage.test.ts b/server/mongo/src/__tests__/storage.test.ts index 8e89d50372..99a408c17a 100644 --- a/server/mongo/src/__tests__/storage.test.ts +++ b/server/mongo/src/__tests__/storage.test.ts @@ -134,7 +134,6 @@ describe('mongo operations', () => { }) it('check add', async () => { - jest.setTimeout(500000) const times: number[] = [] for (let i = 0; i < 50; i++) { const t = Date.now() @@ -153,7 +152,6 @@ describe('mongo operations', () => { }) it('check find by criteria', async () => { - jest.setTimeout(20000) for (let i = 0; i < 50; i++) { await operations.createDoc(taskPlugin.class.Task, '' as Ref, { name: `my-task-${i}`, diff --git a/server/mongo/src/storage.ts b/server/mongo/src/storage.ts index 67e0a96bab..1c3c47ec56 100644 --- a/server/mongo/src/storage.ts +++ b/server/mongo/src/storage.ts @@ -201,9 +201,7 @@ abstract class MongoAdapterBase implements DbAdapter { } return docs }, - close: async () => { - await cursor.close() - } + close: () => cursor.close() } } @@ -262,8 +260,8 @@ abstract class MongoAdapterBase implements DbAdapter { } } - async rawDeleteMany(domain: Domain, query: DocumentQuery): Promise { - await this.db.collection(domain).deleteMany(this.translateRawQuery(query)) + rawDeleteMany(domain: Domain, query: DocumentQuery): Promise { + return this.db.collection(domain).deleteMany(this.translateRawQuery(query)).then() } abstract init (): Promise @@ -280,8 +278,9 @@ abstract class MongoAdapterBase implements DbAdapter { return [] } - async close (): Promise { + close (): Promise { this.client.close() + return Promise.resolve() } private translateQuery( @@ -460,13 +459,13 @@ abstract class MongoAdapterBase implements DbAdapter { return result } - private async fillLookup( + private fillLookup( _class: Ref>, object: any, key: string, fullKey: string, targetObject: any - ): Promise { + ): void { if (targetObject.$lookup === undefined) { targetObject.$lookup = {} } @@ -481,11 +480,11 @@ abstract class MongoAdapterBase implements DbAdapter { } } } else { - targetObject.$lookup[key] = (await this.modelDb.findAll(_class, { _id: targetObject[key] }))[0] + targetObject.$lookup[key] = this.modelDb.findAllSync(_class, { _id: targetObject[key] })[0] } } - private async fillLookupValue( + private fillLookupValue( ctx: MeasureContext, clazz: Ref>, lookup: Lookup | undefined, @@ -496,11 +495,11 @@ abstract class MongoAdapterBase implements DbAdapter { field: string domain: Domain } - ): Promise { + ): void { if (lookup === undefined && domainLookup === undefined) return for (const key in lookup) { if (key === '_id') { - await this.fillReverseLookup(clazz, lookup, object, parent, parentObject) + this.fillReverseLookup(clazz, lookup, object, parent, parentObject) continue } const value = (lookup as any)[key] @@ -509,10 +508,10 @@ abstract class MongoAdapterBase implements DbAdapter { const targetObject = parentObject ?? object if (Array.isArray(value)) { const [_class, nested] = value - await this.fillLookup(_class, object, key, fullKey, targetObject) - await this.fillLookupValue(ctx, _class, nested, object, fullKey, targetObject.$lookup[key]) + this.fillLookup(_class, object, key, fullKey, targetObject) + this.fillLookupValue(ctx, _class, nested, object, fullKey, targetObject.$lookup[key]) } else { - await this.fillLookup(value, object, key, fullKey, targetObject) + this.fillLookup(value, object, key, fullKey, targetObject) } } if (domainLookup !== undefined) { @@ -525,13 +524,13 @@ abstract class MongoAdapterBase implements DbAdapter { } } - private async fillReverseLookup( + private fillReverseLookup( clazz: Ref>, lookup: ReverseLookups, object: any, parent?: string, parentObject?: any - ): Promise { + ): void { const targetObject = parentObject ?? object if (targetObject.$lookup === undefined) { targetObject.$lookup = {} @@ -554,17 +553,17 @@ abstract class MongoAdapterBase implements DbAdapter { const arr = object[fullKey] targetObject.$lookup[key] = arr } else { - const arr = await this.modelDb.findAll(_class, { [attr]: targetObject._id }) + const arr = this.modelDb.findAllSync(_class, { [attr]: targetObject._id }) targetObject.$lookup[key] = arr } } } - private async fillSortPipeline( + private fillSortPipeline( clazz: Ref>, options: FindOptions | undefined, pipeline: any[] - ): Promise { + ): void { if (options?.sort !== undefined) { const sort = {} as any for (const _key in options.sort) { @@ -630,7 +629,7 @@ abstract class MongoAdapterBase implements DbAdapter { } } const totalPipeline: any[] = [...pipeline] - await this.fillSortPipeline(clazz, options, pipeline) + this.fillSortPipeline(clazz, options, pipeline) if (options?.limit !== undefined || typeof query._id === 'string') { pipeline.push({ $limit: options?.limit ?? 1 }) } @@ -652,14 +651,11 @@ abstract class MongoAdapterBase implements DbAdapter { let result: WithLookup[] = [] let total = options?.total === true ? 0 : -1 try { - await ctx.with( + result = await ctx.with( 'aggregate', { clazz }, - async (ctx) => { - result = await toArray(cursor) - }, + (ctx) => toArray(cursor), () => ({ - size: result.length, domain, pipeline, clazz @@ -670,8 +666,8 @@ abstract class MongoAdapterBase implements DbAdapter { throw e } for (const row of result) { - await ctx.with('fill-lookup', {}, async (ctx) => { - await this.fillLookupValue(ctx, clazz, options?.lookup, row, undefined, undefined, options.domainLookup) + ctx.withSync('fill-lookup', {}, (ctx) => { + this.fillLookupValue(ctx, clazz, options?.lookup, row, undefined, undefined, options.domainLookup) }) if (row.$lookup !== undefined) { for (const [, v] of Object.entries(row.$lookup)) { @@ -688,7 +684,7 @@ abstract class MongoAdapterBase implements DbAdapter { const arr = await ctx.with( 'aggregate-total', {}, - async (ctx) => await toArray(totalCursor), + (ctx) => toArray(totalCursor), () => ({ domain, pipeline, @@ -800,13 +796,13 @@ abstract class MongoAdapterBase implements DbAdapter { } @withContext('groupBy') - async groupBy( + groupBy( ctx: MeasureContext, domain: Domain, field: string, query?: DocumentQuery ): Promise> { - const result = await ctx.with('groupBy', { domain }, async (ctx) => { + return ctx.with('groupBy', { domain }, async (ctx) => { const coll = this.collection(domain) const grResult = await coll .aggregate([ @@ -821,10 +817,9 @@ abstract class MongoAdapterBase implements DbAdapter { .toArray() return new Map(grResult.map((it) => [it._id as unknown as T, it.count])) }) - return result } - async findAll( + findAll( ctx: MeasureContext, _class: Ref>, query: DocumentQuery, @@ -833,7 +828,7 @@ abstract class MongoAdapterBase implements DbAdapter { const stTime = Date.now() const mongoQuery = this.translateQuery(_class, query, options) const fQuery = { ...mongoQuery.base, ...mongoQuery.lookup } - return await addOperation(ctx, 'find-all', {}, async () => { + return addOperation(ctx, 'find-all', {}, async () => { const st = Date.now() let result: FindResult const domain = options?.domain ?? this.hierarchy.getDomain(_class) @@ -911,15 +906,11 @@ abstract class MongoAdapterBase implements DbAdapter { } // Error in case of timeout try { - let res: T[] = [] - await ctx.with( + const res: T[] = await ctx.with( 'find-all', {}, - async (ctx) => { - res = await toArray(cursor) - }, + (ctx) => toArray(cursor), () => ({ - size: res.length, queueTime: stTime - st, mongoQuery, options, @@ -1031,18 +1022,15 @@ abstract class MongoAdapterBase implements DbAdapter { const flush = async (flush = false): Promise => { if (bulkUpdate.size > 1000 || flush) { if (bulkUpdate.size > 0) { - await ctx.with( - 'bulk-write-find', - {}, - async () => - await coll.bulkWrite( - Array.from(bulkUpdate.entries()).map((it) => ({ - updateOne: { - filter: { _id: it[0], '%hash%': null }, - update: { $set: { '%hash%': it[1] } } - } - })) - ) + await ctx.with('bulk-write-find', {}, () => + coll.bulkWrite( + Array.from(bulkUpdate.entries()).map((it) => ({ + updateOne: { + filter: { _id: it[0], '%hash%': null }, + update: { $set: { '%hash%': it[1] } } + } + })) + ) ) } bulkUpdate.clear() @@ -1064,12 +1052,12 @@ abstract class MongoAdapterBase implements DbAdapter { } ) } - let d = await ctx.with('next', { mode }, async () => await iterator.next()) + let d = await ctx.with('next', { mode }, () => iterator.next()) if (d == null && mode === 'hashed' && recheck !== true) { mode = 'non-hashed' await iterator.close() iterator = coll.find({ '%hash%': { $in: ['', null] } }) - d = await ctx.with('next', { mode }, async () => await iterator.next()) + d = await ctx.with('next', { mode }, () => iterator.next()) } const result: DocInfo[] = [] if (d != null) { @@ -1078,18 +1066,12 @@ abstract class MongoAdapterBase implements DbAdapter { if (iterator.bufferedCount() > 0) { result.push(...iterator.readBufferedDocuments().map((it) => this.toDocInfo(it, bulkUpdate, recheck))) } - await ctx.with('flush', {}, async () => { - await flush() - }) + await ctx.with('flush', {}, () => flush()) return result }, close: async () => { - await ctx.with('flush', {}, async () => { - await flush(true) - }) - await ctx.with('close', {}, async () => { - await iterator.close() - }) + await ctx.with('flush', {}, () => flush(true)) + await ctx.with('close', {}, () => iterator.close()) ctx.end() } } @@ -1131,8 +1113,8 @@ abstract class MongoAdapterBase implements DbAdapter { } } - async load (ctx: MeasureContext, domain: Domain, docs: Ref[]): Promise { - return await ctx.with('load', { domain }, async () => { + load (ctx: MeasureContext, domain: Domain, docs: Ref[]): Promise { + return ctx.with('load', { domain }, async () => { if (docs.length === 0) { return [] } @@ -1142,16 +1124,16 @@ abstract class MongoAdapterBase implements DbAdapter { }) } - async upload (ctx: MeasureContext, domain: Domain, docs: Doc[]): Promise { - await ctx.with('upload', { domain }, async () => { + upload (ctx: MeasureContext, domain: Domain, docs: Doc[]): Promise { + return ctx.with('upload', { domain }, () => { const coll = this.collection(domain) - await uploadDocuments(ctx, docs, coll) + return uploadDocuments(ctx, docs, coll) }) } - async update (ctx: MeasureContext, domain: Domain, operations: Map, DocumentUpdate>): Promise { - await ctx.with('update', { domain }, async () => { + update (ctx: MeasureContext, domain: Domain, operations: Map, DocumentUpdate>): Promise { + return ctx.with('update', { domain }, async () => { const coll = this.collection(domain) // remove old and insert new ones @@ -1163,8 +1145,8 @@ abstract class MongoAdapterBase implements DbAdapter { await ctx.with( 'bulk-update', {}, - async () => { - await coll.bulkWrite( + () => { + return coll.bulkWrite( part.map((it) => { const { $unset, ...set } = it[1] as any if ($unset !== undefined) { @@ -1205,8 +1187,8 @@ abstract class MongoAdapterBase implements DbAdapter { }) } - async clean (ctx: MeasureContext, domain: Domain, docs: Ref[]): Promise { - await ctx.with('clean', {}, async () => { + clean (ctx: MeasureContext, domain: Domain, docs: Ref[]): Promise { + return ctx.with('clean', {}, async () => { if (docs.length > 0) { await this.db.collection(domain).deleteMany({ _id: { $in: docs } }) } diff --git a/server/postgres/src/storage.ts b/server/postgres/src/storage.ts index 412292c046..84c63c2118 100644 --- a/server/postgres/src/storage.ts +++ b/server/postgres/src/storage.ts @@ -402,13 +402,13 @@ abstract class PostgresAdapterBase implements DbAdapter { } } - async findAll( + findAll( ctx: MeasureContext, _class: Ref>, query: DocumentQuery, options?: ServerFindOptions ): Promise> { - return await ctx.with('findAll', { _class }, async () => { + return ctx.with('findAll', { _class }, async () => { try { const domain = translateDomain(options?.domain ?? this.hierarchy.getDomain(_class)) const sqlChunks: string[] = [] @@ -1142,9 +1142,9 @@ abstract class PostgresAdapterBase implements DbAdapter { const flush = async (flush = false): Promise => { if (bulkUpdate.size > 1000 || flush) { if (bulkUpdate.size > 0) { - await ctx.with('bulk-write-find', {}, async () => { + await ctx.with('bulk-write-find', {}, () => { const updates = new Map(Array.from(bulkUpdate.entries()).map((it) => [it[0], { '%hash%': it[1] }])) - await this.update(ctx, domain, updates) + return this.update(ctx, domain, updates) }) } bulkUpdate.clear() @@ -1162,12 +1162,12 @@ abstract class PostgresAdapterBase implements DbAdapter { await init('_id, data', "'%hash%' IS NOT NULL AND '%hash%' <> ''") initialized = true } - let docs = await ctx.with('next', { mode }, async () => await next(50)) + let docs = await ctx.with('next', { mode }, () => next(50)) if (docs.length === 0 && mode === 'hashed') { await close(cursorName) mode = 'non_hashed' await init('*', "'%hash%' IS NULL OR '%hash%' = ''") - docs = await ctx.with('next', { mode }, async () => await next(50)) + docs = await ctx.with('next', { mode }, () => next(50)) } if (docs.length === 0) { return [] @@ -1190,9 +1190,7 @@ abstract class PostgresAdapterBase implements DbAdapter { bulkUpdate.set(d._id, `${digest}|${size.toString(16)}`) - await ctx.with('flush', {}, async () => { - await flush() - }) + await ctx.with('flush', {}, () => flush()) result.push({ id: d._id, hash: digest, @@ -1209,17 +1207,15 @@ abstract class PostgresAdapterBase implements DbAdapter { return result }, close: async () => { - await ctx.with('flush', {}, async () => { - await flush(true) - }) + await ctx.with('flush', {}, () => flush(true)) await close(cursorName) ctx.end() } } } - async load (ctx: MeasureContext, domain: Domain, docs: Ref[]): Promise { - return await ctx.with('load', { domain }, async () => { + load (ctx: MeasureContext, domain: Domain, docs: Ref[]): Promise { + return ctx.with('load', { domain }, async () => { if (docs.length === 0) { return [] } @@ -1230,8 +1226,8 @@ abstract class PostgresAdapterBase implements DbAdapter { }) } - async upload (ctx: MeasureContext, domain: Domain, docs: Doc[]): Promise { - await ctx.with('upload', { domain }, async (ctx) => { + upload (ctx: MeasureContext, domain: Domain, docs: Doc[]): Promise { + return ctx.with('upload', { domain }, async (ctx) => { const arr = docs.concat() const fields = getDocFieldsByDomains(domain) const filedsWithData = [...fields, 'data'] @@ -1282,15 +1278,15 @@ abstract class PostgresAdapterBase implements DbAdapter { await connection`DELETE FROM ${connection(translateDomain(domain))} WHERE _id = ANY(${docs}) AND "workspaceId" = ${this.workspaceId.name}` } - async groupBy( + groupBy( ctx: MeasureContext, domain: Domain, field: string, query?: DocumentQuery

): Promise> { - const connection = (await this.getConnection(ctx)) ?? this.client const key = isDataField(domain, field) ? `data ->> '${field}'` : `"${field}"` - const result = await ctx.with('groupBy', { domain }, async (ctx) => { + return ctx.with('groupBy', { domain }, async (ctx) => { + const connection = (await this.getConnection(ctx)) ?? this.client try { const result = await connection.unsafe( `SELECT DISTINCT ${key} as ${field}, Count(*) AS count FROM ${translateDomain(domain)} WHERE ${this.buildRawQuery(domain, query ?? {})} GROUP BY ${key}` @@ -1301,13 +1297,12 @@ abstract class PostgresAdapterBase implements DbAdapter { throw err } }) - return result } - async update (ctx: MeasureContext, domain: Domain, operations: Map, DocumentUpdate>): Promise { + update (ctx: MeasureContext, domain: Domain, operations: Map, DocumentUpdate>): Promise { const ids = Array.from(operations.keys()) - await this.withConnection(ctx, async (client) => { - await this.retryTxn(client, async (client) => { + return this.withConnection(ctx, (client) => { + return this.retryTxn(client, async (client) => { try { const res = await client`SELECT * FROM ${client(translateDomain(domain))} WHERE _id = ANY(${ids}) AND "workspaceId" = ${this.workspaceId.name} FOR UPDATE` @@ -1476,17 +1471,17 @@ class PostgresAdapter extends PostgresAdapterBase { protected async txCreateDoc (ctx: MeasureContext, tx: TxCreateDoc): Promise { const doc = TxProcessor.createDoc2Doc(tx) - return await ctx.with('create-doc', { _class: doc._class }, async (_ctx) => { - return await this.insert(_ctx, this.hierarchy.getDomain(doc._class), [doc]) + return await ctx.with('create-doc', { _class: doc._class }, (_ctx) => { + return this.insert(_ctx, this.hierarchy.getDomain(doc._class), [doc]) }) } - protected async txUpdateDoc (ctx: MeasureContext, tx: TxUpdateDoc): Promise { - return await ctx.with('tx-update-doc', { _class: tx.objectClass }, async (_ctx) => { + protected txUpdateDoc (ctx: MeasureContext, tx: TxUpdateDoc): Promise { + return ctx.with('tx-update-doc', { _class: tx.objectClass }, (_ctx) => { if (isOperator(tx.operations)) { let doc: Doc | undefined const ops: any = { '%hash%': null, ...tx.operations } - return await _ctx.with( + return _ctx.with( 'update with operations', { operations: JSON.stringify(Object.keys(tx.operations)) }, async (ctx) => { @@ -1517,17 +1512,13 @@ class PostgresAdapter extends PostgresAdapterBase { } ) } else { - return await this.updateDoc(_ctx, tx, tx.retrieve ?? false) + return this.updateDoc(_ctx, tx, tx.retrieve ?? false) } }) } - private async updateDoc( - ctx: MeasureContext, - tx: TxUpdateDoc, - retrieve: boolean - ): Promise { - return await ctx.with('update jsonb_set', {}, async (_ctx) => { + private updateDoc(ctx: MeasureContext, tx: TxUpdateDoc, retrieve: boolean): Promise { + return ctx.with('update jsonb_set', {}, async (_ctx) => { const updates: string[] = ['"modifiedBy" = $1', '"modifiedOn" = $2'] const params: any[] = [tx.modifiedBy, tx.modifiedOn, tx.objectId, this.workspaceId.name] let paramsIndex = 5 @@ -1555,12 +1546,12 @@ class PostgresAdapter extends PostgresAdapterBase { } await this.withConnection(ctx, async (connection) => { try { - await this.retryTxn(connection, async (client) => { - await client.unsafe( + await this.retryTxn(connection, (client) => + client.unsafe( `UPDATE ${translateDomain(this.hierarchy.getDomain(tx.objectClass))} SET ${updates.join(', ')} WHERE _id = $3 AND "workspaceId" = $4`, params ) - }) + ) if (retrieve) { const object = await this.findDoc(_ctx, connection, tx.objectClass, tx.objectId) return { object } @@ -1573,14 +1564,14 @@ class PostgresAdapter extends PostgresAdapterBase { }) } - private async findDoc ( + private findDoc ( ctx: MeasureContext, client: postgres.Sql | postgres.ReservedSql, _class: Ref>, _id: Ref, forUpdate: boolean = false ): Promise { - return await ctx.with('find-doc', { _class }, async () => { + return ctx.with('find-doc', { _class }, async () => { const res = await client`SELECT * FROM ${this.client(translateDomain(this.hierarchy.getDomain(_class)))} WHERE _id = ${_id} AND "workspaceId" = ${this.workspaceId.name} ${ forUpdate ? client` FOR UPDATE` : client`` @@ -1595,9 +1586,11 @@ class PostgresAdapter extends PostgresAdapterBase { await ctx.with('tx-remove-doc', { _class: tx.objectClass }, async (_ctx) => { const domain = translateDomain(this.hierarchy.getDomain(tx.objectClass)) await this.withConnection(_ctx, async (connection) => { - await this.retryTxn(connection, async (client) => { - await client`DELETE FROM ${client(domain)} WHERE _id = ${tx.objectId} AND "workspaceId" = ${this.workspaceId.name}` - }) + await this.retryTxn( + connection, + (client) => + client`DELETE FROM ${client(domain)} WHERE _id = ${tx.objectId} AND "workspaceId" = ${this.workspaceId.name}` + ) }) }) return {} diff --git a/server/s3/src/index.ts b/server/s3/src/index.ts index c76680c5f4..2801aef391 100644 --- a/server/s3/src/index.ts +++ b/server/s3/src/index.ts @@ -341,7 +341,7 @@ export class S3Service implements StorageAdapter { } @withContext('put') - async put ( + put ( ctx: MeasureContext, workspaceId: WorkspaceId, objectName: string, @@ -350,7 +350,7 @@ export class S3Service implements StorageAdapter { size?: number ): Promise { if (size !== undefined && size < 1024 * 1024 * 5) { - return await ctx.with( + return ctx.with( 'simple-put', {}, async () => { @@ -371,7 +371,7 @@ export class S3Service implements StorageAdapter { ) // Less 5Mb } else { - return await ctx.with( + return ctx.with( 'multipart-upload', {}, async () => { diff --git a/server/server/src/sessionManager.ts b/server/server/src/sessionManager.ts index ebc007efc5..e517b14ea7 100644 --- a/server/server/src/sessionManager.ts +++ b/server/server/src/sessionManager.ts @@ -90,7 +90,7 @@ class TSessionManager implements SessionManager { checkInterval: any sessions = new Map() - reconnectIds = new Map() + reconnectIds = new Set() maintenanceTimer: any timeMinutes = 0 @@ -180,8 +180,26 @@ class TSessionManager implements SessionManager { const now = Date.now() for (const [wsId, workspace] of this.workspaces.entries()) { if (this.ticks % (60 * ticksPerSecond) === workspace.tickHash) { - // update account lastVisit every minute per every workspace.∏ - void this.getWorkspaceInfo(this.ctx, workspace.token) + try { + // update account lastVisit every minute per every workspace.∏ + void this.getWorkspaceInfo(this.ctx, workspace.token).catch(() => { + // Ignore + }) + } catch (err: any) { + // Ignore + } + } + + for (const [k, v] of Array.from(workspace.tickHandlers.entries())) { + v.ticks-- + if (v.ticks === 0) { + workspace.tickHandlers.delete(k) + try { + v.operation() + } catch (err: any) { + Analytics.handleError(err) + } + } } for (const s of workspace.sessions) { @@ -465,7 +483,9 @@ class TSessionManager implements SessionManager { // We do not need to wait for set-status, just return session to client const _workspace = workspace - void ctx.with('set-status', {}, (ctx) => this.trySetStatus(ctx, session, true, _workspace.workspaceId)) + void ctx + .with('set-status', {}, (ctx) => this.trySetStatus(ctx, session, true, _workspace.workspaceId)) + .catch(() => {}) if (this.timeMinutes > 0) { ws.send(ctx, { result: this.createMaintenanceWarning() }, session.binaryMode, session.useCompression) @@ -638,6 +658,7 @@ class TSessionManager implements SessionManager { branding, workspaceInitCompleted: false, tickHash: this.tickCounter % ticksPerSecond, + tickHandlers: new Map(), token: generateToken(systemAccountEmail, token.workspace) } this.workspaces.set(toWorkspaceString(token.workspace), workspace) @@ -714,21 +735,25 @@ class TSessionManager implements SessionManager { this.sessions.delete(ws.id) if (workspace !== undefined) { workspace.sessions.delete(sessionRef.session.sessionId) - } - this.reconnectIds.set( - sessionRef.session.sessionId, - setTimeout(() => { - this.reconnectIds.delete(sessionRef.session.sessionId) - const user = sessionRef.session.getUser() - if (workspace !== undefined) { - const another = Array.from(workspace.sessions.values()).findIndex((p) => p.session.getUser() === user) - if (another === -1 && !workspace.upgrade) { - void this.trySetStatus(workspace.context, sessionRef.session, false, workspace.workspaceId) + workspace.tickHandlers.set(sessionRef.session.sessionId, { + ticks: this.timeouts.reconnectTimeout * ticksPerSecond, + operation: () => { + this.reconnectIds.delete(sessionRef.session.sessionId) + + const user = sessionRef.session.getUser() + if (workspace !== undefined) { + const another = Array.from(workspace.sessions.values()).findIndex((p) => p.session.getUser() === user) + if (another === -1 && !workspace.upgrade) { + void this.trySetStatus(workspace.context, sessionRef.session, false, workspace.workspaceId).catch( + () => {} + ) + } } } - }, this.timeouts.reconnectTimeout) - ) + }) + this.reconnectIds.add(sessionRef.session.sessionId) + } try { sessionRef.socket.close() } catch (err) { @@ -898,8 +923,8 @@ class TSessionManager implements SessionManager { const reqId = generateId() const st = Date.now() - try { - void userCtx.with(`🧭 ${backupMode ? 'handleBackup' : 'handleRequest'}`, {}, async (ctx) => { + void userCtx + .with(`🧭 ${backupMode ? 'handleBackup' : 'handleRequest'}`, {}, async (ctx) => { if (request.time != null) { const delta = Date.now() - request.time requestCtx.measure('msg-receive-delta', delta) @@ -950,8 +975,6 @@ class TSessionManager implements SessionManager { } const reconnect = this.reconnectIds.has(service.sessionId) if (reconnect) { - const reconnectTimeout = this.reconnectIds.get(service.sessionId) - clearTimeout(reconnectTimeout) this.reconnectIds.delete(service.sessionId) } const helloResponse: HelloResponse = { @@ -1020,10 +1043,10 @@ class TSessionManager implements SessionManager { ) } }) - } finally { - userCtx.end() - service.requests.delete(reqId) - } + .finally(() => { + userCtx.end() + service.requests.delete(reqId) + }) } } diff --git a/server/tool/src/index.ts b/server/tool/src/index.ts index b2220d83e0..2fb7408b86 100644 --- a/server/tool/src/index.ts +++ b/server/tool/src/index.ts @@ -264,9 +264,7 @@ export async function upgradeModel ( const t = Date.now() try { - await ctx.with(op[0], {}, async (ctx) => { - await preMigrate(preMigrateClient, logger) - }) + await ctx.with(op[0], {}, (ctx) => preMigrate(preMigrateClient, logger)) } catch (err: any) { logger.error(`error during pre-migrate: ${op[0]} ${err.message}`, err) throw err @@ -309,9 +307,7 @@ export async function upgradeModel ( for (const op of migrateOperations) { try { const t = Date.now() - await ctx.with(op[0], {}, async () => { - await op[1].migrate(migrateClient, logger) - }) + await ctx.with(op[0], {}, () => op[1].migrate(migrateClient, logger)) const tdelta = Date.now() - t if (tdelta > 0) { logger.log('migrate:', { workspaceId: workspaceId.name, operation: op[0], time: Date.now() - t }) @@ -340,9 +336,7 @@ export async function upgradeModel ( let i = 0 for (const op of migrateOperations) { const t = Date.now() - await ctx.with(op[0], {}, async () => { - await op[1].upgrade(migrateState, async () => connection, logger) - }) + await ctx.with(op[0], {}, () => op[1].upgrade(migrateState, async () => connection, logger)) const tdelta = Date.now() - t if (tdelta > 0) { logger.log('upgrade:', { operation: op[0], time: tdelta, workspaceId: workspaceId.name }) diff --git a/services/github/pod-github/src/platform.ts b/services/github/pod-github/src/platform.ts index 4b8403896b..edf5f4a93d 100644 --- a/services/github/pod-github/src/platform.ts +++ b/services/github/pod-github/src/platform.ts @@ -141,9 +141,7 @@ export class PlatformWorker { for (const [workspace, users] of workspaces) { const worker = this.clients.get(workspace) if (worker !== undefined) { - await this.ctx.with('syncUsers', {}, async (ctx) => { - await worker.syncUserData(ctx, users) - }) + await this.ctx.with('syncUsers', {}, (ctx) => worker.syncUserData(ctx, users)) } } this.periodicSyncPromise = undefined diff --git a/services/github/pod-github/src/sync/issues.ts b/services/github/pod-github/src/sync/issues.ts index 818d97a69b..f4ccd33e7e 100644 --- a/services/github/pod-github/src/sync/issues.ts +++ b/services/github/pod-github/src/sync/issues.ts @@ -998,8 +998,8 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan const response: any = await this.ctx.with( 'graphql.listIssue', { prj: prj.name, repo: repo.name }, - async () => - await integration.octokit.graphql( + () => + integration.octokit.graphql( `query listIssues { nodes(ids: [${idsp}] ) { ... on Issue { diff --git a/services/github/pod-github/src/worker.ts b/services/github/pod-github/src/worker.ts index e4ad54f87e..69cfe74e67 100644 --- a/services/github/pod-github/src/worker.ts +++ b/services/github/pod-github/src/worker.ts @@ -1125,8 +1125,8 @@ export class GithubWorker implements IntegrationManager { const docs = await this.ctx.with( 'find-doc-sync-info', {}, - async (ctx) => - await this._client.findAll( + (ctx) => + this._client.findAll( github.class.DocSyncInfo, { needSync: { $ne: githubSyncVersion }, diff --git a/services/github/server-github-model/src/index.ts b/services/github/server-github-model/src/index.ts index cebd56f8b4..c1d6c06dbe 100644 --- a/services/github/server-github-model/src/index.ts +++ b/services/github/server-github-model/src/index.ts @@ -14,7 +14,6 @@ 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 })