Rework triggers with arrays (#7168)

This commit is contained in:
Andrey Sobolev 2024-11-13 21:25:57 +07:00 committed by GitHub
parent 3837e68589
commit e493d35ac6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
78 changed files with 2006 additions and 1871 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,11 +295,8 @@ 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(
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 }
@ -364,12 +356,16 @@ async function tryLoadModel (
}
// Save concatenated
void (await ctx.with('persistence-store', {}, (ctx) =>
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)
})
}

View File

@ -47,6 +47,8 @@ const consoleLogger = (logParams: Record<string, any>): 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<T>
}
return Promise.resolve(value)
}
} finally {

View File

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

View File

@ -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<Ref<DocIndexState>, DocIndexState>
}
/**

View File

@ -1317,16 +1317,10 @@ export class LiveQuery implements WithTx, Client {
return result
}
triggerInProgress = true
private async checkUpdateEvents (evt: TxWorkspaceEvent, trigger = true): Promise<void> {
if (this.triggerInProgress) {
this.triggerInProgress = false
const h = this.client.getHierarchy()
function hasClass (q: Query, classes: Ref<Class<Doc>>[]): boolean {
return (
classes.includes(q._class) || classes.some((it) => h.isDerived(q._class, it) || h.isDerived(it, q._class))
)
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
@ -1391,11 +1385,6 @@ export class LiveQuery implements WithTx, Client {
}
}
}
setTimeout(() => {
this.triggerInProgress = true
void this.checkUpdateEvents(evt, false)
}, 20000)
}
}
private async changePrivateHandler (evt: TxWorkspaceEvent): Promise<void> {

View File

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

View File

@ -53,7 +53,8 @@ import {
import { ReferenceTrigger } from './references'
import { getAttrName, getCollectionAttribute, getDocUpdateAction, getTxAttributesUpdates } from './utils'
export async function OnReactionChanged (originTx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnReactionChanged (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
for (const originTx of txes) {
const tx = originTx as TxCollectionCUD<ActivityMessage, Reaction>
const innerTx = TxProcessor.extractTx(tx) as TxCUD<Reaction>
@ -61,13 +62,14 @@ export async function OnReactionChanged (originTx: Tx, control: TriggerControl):
const txes = await createReactionNotifications(tx, control)
await control.apply(control.ctx, txes)
return []
continue
}
if (innerTx._class === core.class.TxRemoveDoc) {
const txes = await removeReactionNotifications(tx, control)
await control.apply(control.ctx, txes)
return []
continue
}
}
return []
@ -304,11 +306,8 @@ export async function generateDocUpdateMessages (
switch (tx._class) {
case core.class.TxCreateDoc: {
const doc = TxProcessor.createDoc2Doc(tx as TxCreateDoc<Doc>)
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,11 +350,8 @@ export async function generateDocUpdateMessages (
}
if (doc !== undefined) {
objectCache?.docs?.set(tx.objectId, doc)
return await ctx.with(
'pushDocUpdateMessages',
{},
async (ctx) =>
await pushDocUpdateMessages(
return await ctx.with('pushDocUpdateMessages', {}, (ctx) =>
pushDocUpdateMessages(
ctx,
control,
res,

View File

@ -791,12 +791,17 @@ async function ActivityReferenceRemove (tx: Tx, etx: TxCUD<Doc>, control: Trigge
/**
* @public
*/
export async function ReferenceTrigger (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
export async function ReferenceTrigger (txes: TxCUD<Doc>[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const etx = TxProcessor.extractTx(tx) as TxCUD<Doc>
if (control.hierarchy.isDerived(etx.objectClass, activity.class.ActivityReference)) return []
if (control.hierarchy.isDerived(etx.objectClass, notification.class.InboxNotification)) return []
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)))
@ -807,5 +812,6 @@ export async function ReferenceTrigger (tx: TxCUD<Doc>, control: TriggerControl)
if (etx._class === core.class.TxRemoveDoc) {
result.push(...(await ActivityReferenceRemove(tx, etx, control)))
}
}
return result
}

View File

@ -278,7 +278,7 @@ export async function OnMessageSend (
return []
}
export async function OnMention (tx: TxCreateDoc<MentionInboxNotification>, control: TriggerControl): Promise<Tx[]> {
export async function OnMention (tx: TxCreateDoc<MentionInboxNotification>[], control: TriggerControl): Promise<Tx[]> {
// 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<MentionInboxNotification>, cont
}
export async function OnMessageNotified (
tx: TxCreateDoc<ActivityInboxNotification>,
tx: TxCreateDoc<ActivityInboxNotification>[],
control: TriggerControl
): Promise<Tx[]> {
// Note: temporally commented until open ai will be added
@ -363,21 +363,22 @@ export async function OnMessageNotified (
return []
}
export async function OnUserStatus (originTx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnUserStatus (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
for (const originTx of txes) {
const tx = TxProcessor.extractTx(originTx) as TxCUD<UserStatus>
if (
tx.objectClass !== core.class.UserStatus ||
![core.class.TxCreateDoc, core.class.TxUpdateDoc].includes(tx._class)
) {
return []
continue
}
if (tx._class === core.class.TxCreateDoc) {
const createTx = tx as TxCreateDoc<UserStatus>
const status = TxProcessor.createDoc2Doc(createTx)
if (status.user === aiBot.account.AIBot || status.user === core.account.System || !status.online) {
return []
continue
}
}
@ -385,22 +386,23 @@ export async function OnUserStatus (originTx: Tx, control: TriggerControl): Prom
const updateTx = tx as TxUpdateDoc<UserStatus>
const val = updateTx.operations.online
if (val !== true) {
return []
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 []
continue
}
}
const account = control.modelDb.findAllSync(contact.class.PersonAccount, { email: aiBotAccountEmail })[0]
if (account !== undefined) {
return []
continue
}
await createAccountRequest(control.workspace, control.ctx)
}
return []
}

View File

@ -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<Tx[]> {
const toDelete: string[] = []
for (const tx of txes) {
const rmTx = TxProcessor.extractTx(tx) as TxRemoveDoc<Attachment>
// Obtain document being deleted.
const attach = removedMap.get(rmTx.objectId) as Attachment
if (attach === undefined) {
return []
continue
}
toDelete.push(attach.file)
}
if (toDelete.length > 0) {
await storageAdapter.remove(ctx, workspace, toDelete)
}
await storageAdapter.remove(ctx, workspace, [attach.file])
return []
}

View File

@ -83,11 +83,14 @@ export async function ReminderTextPresenter (doc: Doc, control: TriggerControl):
/**
* @public
*/
export async function OnPersonAccountCreate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnPersonAccountCreate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const ctx = TxProcessor.extractTx(tx) as TxCreateDoc<PersonAccount>
const user = TxProcessor.createDoc2Doc(ctx)
const res: TxCreateDoc<Calendar> = control.txFactory.createTxCreateDoc(
result.push(
control.txFactory.createTxCreateDoc(
calendar.class.Calendar,
calendar.space.Calendar,
{
@ -99,7 +102,9 @@ export async function OnPersonAccountCreate (tx: Tx, control: TriggerControl): P
undefined,
user._id
)
return [res]
)
}
return result
}
function getCalendar (calendars: Calendar[], person: Ref<PersonAccount>): Ref<Calendar> | undefined {
@ -118,17 +123,20 @@ function getEventPerson (current: Event, calendars: Calendar[], control: Trigger
return acc.person
}
async function OnEvent (tx: Tx, control: TriggerControl): Promise<Tx[]> {
async function OnEvent (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const ctx = TxProcessor.extractTx(tx) as TxCUD<Event>
if (ctx._class === core.class.TxCreateDoc) {
return await onEventCreate(ctx as TxCreateDoc<Event>, control)
result.push(...(await onEventCreate(ctx as TxCreateDoc<Event>, control)))
} else if (ctx._class === core.class.TxUpdateDoc) {
return await onEventUpdate(ctx as TxUpdateDoc<Event>, control)
result.push(...(await onEventUpdate(ctx as TxUpdateDoc<Event>, control)))
} else if (ctx._class === core.class.TxRemoveDoc) {
return await onRemoveEvent(ctx as TxRemoveDoc<Event>, control)
result.push(...(await onRemoveEvent(ctx as TxRemoveDoc<Event>, control)))
}
}
return []
return result
}
async function onEventUpdate (ctx: TxUpdateDoc<Event>, control: TriggerControl): Promise<Tx[]> {

View File

@ -203,16 +203,20 @@ async function OnChatMessageCreated (ctx: MeasureContext, tx: TxCUD<Doc>, contro
return res
}
async function ChatNotificationsHandler (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
async function ChatNotificationsHandler (txes: TxCUD<Doc>[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const actualTx = TxProcessor.extractTx(tx) as TxCreateDoc<ChatMessage>
if (actualTx._class !== core.class.TxCreateDoc) {
return []
continue
}
const chatMessage = TxProcessor.createDoc2Doc(actualTx)
return await createCollaboratorNotifications(control.ctx, tx, control, [chatMessage])
result.push(...(await createCollaboratorNotifications(control.ctx, tx, control, [chatMessage])))
}
return result
}
function joinChannel (control: TriggerControl, channel: Channel, user: Ref<Account>): Tx[] {
@ -263,8 +267,9 @@ async function OnThreadMessageDeleted (tx: Tx, control: TriggerControl): Promise
/**
* @public
*/
export async function ChunterTrigger (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
export async function ChunterTrigger (txes: TxCUD<Doc>[], control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = []
for (const tx of txes) {
const actualTx = TxProcessor.extractTx(tx) as TxCreateDoc<ChatMessage>
if (
@ -285,6 +290,7 @@ export async function ChunterTrigger (tx: TxCUD<Doc>, control: TriggerControl):
) {
res.push(...(await control.ctx.with('OnChatMessageCreated', {}, (ctx) => OnChatMessageCreated(ctx, tx, control))))
}
}
return res
}
@ -342,12 +348,13 @@ export async function getChunterNotificationContent (
}
}
async function OnChatMessageRemoved (tx: TxCollectionCUD<Doc, ChatMessage>, control: TriggerControl): Promise<Tx[]> {
async function OnChatMessageRemoved (txes: TxCollectionCUD<Doc, ChatMessage>[], control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = []
for (const tx of txes) {
if (tx.tx._class !== core.class.TxRemoveDoc) {
return []
continue
}
const res: Tx[] = []
const notifications = await control.findAll(control.ctx, notification.class.InboxNotification, {
attachedTo: tx.tx.objectId
})
@ -355,7 +362,7 @@ async function OnChatMessageRemoved (tx: TxCollectionCUD<Doc, ChatMessage>, cont
notifications.forEach((notification) => {
res.push(control.txFactory.createTxRemoveDoc(notification._class, notification.space, notification._id))
})
}
return res
}
@ -470,9 +477,12 @@ export async function syncChat (control: TriggerControl, status: UserStatus, dat
await control.apply(control.ctx, res, true)
}
async function OnUserStatus (originTx: TxCUD<UserStatus>, control: TriggerControl): Promise<Tx[]> {
async function OnUserStatus (txes: TxCUD<UserStatus>[], control: TriggerControl): Promise<Tx[]> {
for (const originTx of txes) {
const tx = TxProcessor.extractTx(originTx) as TxCUD<UserStatus>
if (tx.objectClass !== core.class.UserStatus) return []
if (tx.objectClass !== core.class.UserStatus) {
continue
}
if (tx._class === core.class.TxCreateDoc) {
const createTx = tx as TxCreateDoc<UserStatus>
const { online } = createTx.attributes
@ -488,6 +498,7 @@ async function OnUserStatus (originTx: TxCUD<UserStatus>, control: TriggerContro
await syncChat(control, status, originTx.modifiedOn)
}
}
}
return []
}

View File

@ -50,9 +50,10 @@ 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<Tx[]> {
const ctx = tx as TxUpdateDoc<SpaceType>
export async function OnSpaceTypeMembers (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const ctx = tx as TxUpdateDoc<SpaceType>
const newMember = ctx.operations.$push?.members as Ref<Account>
if (newMember !== undefined) {
const spaces = await control.findAll(control.ctx, core.class.Space, { type: ctx.objectId })
@ -79,16 +80,22 @@ export async function OnSpaceTypeMembers (tx: Tx, control: TriggerControl): Prom
result.push(pullTx)
}
}
}
return result
}
export async function OnEmployeeCreate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const mixinTx = tx as TxMixin<Person, Employee>
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<Tx[]> {
const result: Tx[] = []
for (const tx of _txes) {
const mixinTx = tx as TxMixin<Person, Employee>
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),
@ -109,6 +116,7 @@ export async function OnEmployeeCreate (tx: Tx, control: TriggerControl): Promis
result.push(pushTx)
}
}
}
return result
}
@ -142,7 +150,11 @@ async function createPersonSpace (
]
}
export async function OnPersonAccountCreate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnPersonAccountCreate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
const spaces = await control.findAll(control.ctx, core.class.Space, { autoJoin: true })
for (const tx of txes) {
const acc = TxProcessor.createDoc2Doc(tx as TxCreateDoc<PersonAccount>)
const person = (
await control.findAll(
@ -152,10 +164,10 @@ export async function OnPersonAccountCreate (tx: Tx, control: TriggerControl): P
{ limit: 1 }
)
)[0]
if (person === undefined) return []
const spaces = await control.findAll(control.ctx, core.class.Space, { autoJoin: true })
if (person === undefined) {
continue
}
const result: Tx[] = []
const txes = await createPersonSpace([acc._id], person._id, control)
result.push(...txes)
@ -169,6 +181,7 @@ export async function OnPersonAccountCreate (tx: Tx, control: TriggerControl): P
})
result.push(pushTx)
}
}
return result
}
@ -176,18 +189,18 @@ 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<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const rmTx = tx as TxRemoveDoc<Contact>
const removeContact = removedMap.get(rmTx.objectId) as Contact
if (removeContact === undefined) {
return []
continue
}
const result: Tx[] = []
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)
@ -200,6 +213,7 @@ export async function OnContactDelete (
)
result.push(tx)
}
}
return result
}
@ -207,10 +221,10 @@ export async function OnContactDelete (
/**
* @public
*/
export async function OnChannelUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const uTx = tx as TxUpdateDoc<Channel>
export async function OnChannelUpdate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const uTx = tx as TxUpdateDoc<Channel>
if (uTx.operations.$inc?.items !== undefined) {
const doc = (await control.findAll(control.ctx, uTx.objectClass, { _id: uTx.objectId }, { limit: 1 }))[0]
@ -240,6 +254,7 @@ export async function OnChannelUpdate (tx: Tx, control: TriggerControl): Promise
}
}
}
}
return result
}

View File

@ -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,26 +272,32 @@ function updateTemplate (doc: ControlledDocument, olderEffective: ControlledDocu
}
export async function OnDocHasBecomeEffective (
tx: TxUpdateDoc<ControlledDocument>,
txes: TxUpdateDoc<ControlledDocument>[],
control: TriggerControl
): Promise<Tx[]> {
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) {
return []
continue
}
const olderEffective = await getDocsOlderThanDoc(doc, control, [DocumentState.Effective])
return [
result.push(
...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<ControlledDocument>, control: TriggerControl): Promise<Tx[]> {
export async function OnDocDeleted (txes: TxUpdateDoc<ControlledDocument>[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const requests = await control.findAll(control.ctx, documents.class.DocumentRequest, {
attachedTo: tx.objectId,
status: RequestStatus.Active
@ -307,21 +313,24 @@ export async function OnDocDeleted (tx: TxUpdateDoc<ControlledDocument>, control
controlledState: undefined
})
])
}
return []
return result
}
export async function OnDocPlannedEffectiveDateChanged (
tx: TxUpdateDoc<ControlledDocument>,
txes: TxUpdateDoc<ControlledDocument>[],
control: TriggerControl
): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
if (!('plannedEffectiveDate' in tx.operations)) {
return []
continue
}
const doc = (await control.findAll(control.ctx, tx.objectClass, { _id: tx.objectId }, { limit: 1 })).shift()
if (doc === undefined) {
return []
continue
}
// make doc effective immediately if required
@ -330,31 +339,36 @@ export async function OnDocPlannedEffectiveDateChanged (
const factory = new TxFactory(control.txFactory.account)
await control.apply(control.ctx, [makeDocEffective(doc, factory)])
}
}
return []
return result
}
export async function OnDocApprovalRequestApproved (
tx: TxCollectionCUD<ControlledDocument, DocumentApprovalRequest>,
txes: TxCollectionCUD<ControlledDocument, DocumentApprovalRequest>[],
control: TriggerControl
): Promise<Tx[]> {
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) {
return []
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
return []
}
return result
}
/**
* @public
*/
export async function OnWorkspaceOwnerAdded (tx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnWorkspaceOwnerAdded (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
let ownerId: Ref<PersonAccount> | undefined
if (control.hierarchy.isDerived(tx._class, core.class.TxCreateDoc)) {
const createTx = tx as TxCreateDoc<PersonAccount>
@ -371,7 +385,7 @@ export async function OnWorkspaceOwnerAdded (tx: Tx, control: TriggerControl): P
}
if (ownerId === undefined) {
return []
continue
}
const targetSpace = (
@ -381,7 +395,7 @@ export async function OnWorkspaceOwnerAdded (tx: Tx, control: TriggerControl): P
)[0]
if (targetSpace === undefined) {
return []
continue
}
if (
@ -392,10 +406,11 @@ export async function OnWorkspaceOwnerAdded (tx: Tx, control: TriggerControl): P
const updTx = control.txFactory.createTxUpdateDoc(documents.class.OrgSpace, targetSpace.space, targetSpace._id, {
owners: [ownerId]
})
return [updTx]
result.push(updTx)
}
}
return []
return result
}
export async function documentTextPresenter (doc: ControlledDocument): Promise<string> {

View File

@ -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<Tx[]> {
const result: Tx[] = []
const toDelete: string[] = []
for (const tx of txes) {
const rmTx = TxProcessor.extractTx(tx) as TxRemoveDoc<FileVersion>
// Obtain document being deleted.
const version = removedMap.get(rmTx.objectId) as FileVersion
if (version !== undefined) {
await storageAdapter.remove(ctx, workspace, [version.file])
toDelete.push(version.file)
}
}
if (toDelete.length > 0) {
await storageAdapter.remove(ctx, workspace, toDelete)
}
return []
return result
}
/** @public */

View File

@ -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<Tx
control.contextCache.set(indexingCtx, contexts)
}
const docsToUpsert = new Map<Ref<DocIndexState>, 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<Doc, AttachedDoc>
tx = txcol.tx
}
const attrs = new Map<string, AnyAttribute>()
if (TxProcessor.isExtendsCUD(tx._class)) {
const cud = tx as TxCUD<Doc>
@ -52,6 +57,35 @@ export async function OnChange (txes: Tx[], control: TriggerControl): Promise<Tx
// No need, since no indixable fields or attachments.
continue
}
// We need to check if operation has indexable attributes.
if (cud._class === core.class.TxUpdateDoc) {
let hasFulltext = false
// We could skip updates for a lot of transactions if they have non idexable attributes
const upd = cud as TxUpdateDoc<Doc>
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<DocIndexState>, {
_id: cud.objectId as Ref<DocIndexState>,
_class: core.class.DocIndexState,
@ -62,25 +96,8 @@ export async function OnChange (txes: Tx[], control: TriggerControl): Promise<Tx
space: cud.space,
removed: cud._class === core.class.TxRemoveDoc
})
const propagate = getFullTextContext(control.hierarchy, cud.objectClass, contexts).propagate
if (propagate !== undefined && propagate.length > 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<DocIndexState>(
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 []
}

View File

@ -65,9 +65,9 @@ export async function FindMessages (
/**
* @public
*/
export async function OnMessageCreate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = []
export async function OnMessageCreate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const createTx = tx as TxCreateDoc<Message>
const message = TxProcessor.createDoc2Doc<Message>(createTx)
@ -80,11 +80,12 @@ export async function OnMessageCreate (tx: Tx, control: TriggerControl): Promise
const tx = control.txFactory.createTxUpdateDoc(channel._class, channel.space, channel._id, {
lastMessage: message.sendOn
})
res.push(tx)
result.push(tx)
}
}
}
return res
return result
}
/**

View File

@ -35,24 +35,27 @@ import view from '@hcengineering/view'
/**
* @public
*/
export async function OnPublicLinkCreate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = []
export async function OnPublicLinkCreate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const extractedTx = TxProcessor.extractTx(tx)
const createTx = extractedTx as TxCreateDoc<PublicLink>
const link = TxProcessor.createDoc2Doc<PublicLink>(createTx)
if (link.url !== '') return res
if (link.url !== '') {
continue
}
const resTx = control.txFactory.createTxUpdateDoc(link._class, link.space, link._id, {
url: generateUrl(link._id, control.workspace, control.branding?.front)
})
res.push(resTx)
result.push(resTx)
}
return res
return result
}
export function getPublicLinkUrl (workspace: WorkspaceIdWithUrl, brandedFront?: string): string {

View File

@ -131,11 +131,15 @@ function getTxes (
/**
* @public
*/
export async function OnDepartmentStaff (tx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnDepartmentStaff (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const ctx = TxProcessor.extractTx(tx) as TxMixin<Employee, Staff>
const targetAccount = control.modelDb.getAccountByPersonId(ctx.objectId) as PersonAccount[]
if (targetAccount.length === 0) return []
if (targetAccount.length === 0) {
continue
}
if (ctx.attributes.department !== undefined) {
const lastDepartment = await getOldDepartment(ctx, control)
@ -144,50 +148,60 @@ export async function OnDepartmentStaff (tx: Tx, control: TriggerControl): Promi
if (departmentId === null) {
if (lastDepartment !== undefined) {
const removed = await buildHierarchy(lastDepartment, control)
return getTxes(
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) {
return getTxes(
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)
return getTxes(
result.push(
...getTxes(
control.txFactory,
targetAccount.map((it) => it._id),
added,
removed
)
)
}
}
}
return []
return result
}
/**
* @public
*/
export async function OnDepartmentRemove (tx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnDepartmentRemove (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const ctx = TxProcessor.extractTx(tx) as TxRemoveDoc<Department>
const department = control.removedMap.get(ctx.objectId) as Department
if (department === undefined) return []
const res: Tx[] = []
if (department === undefined) {
continue
}
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))
result.push(control.txFactory.createTxRemoveDoc(dep._class, dep.space, dep._id))
}
const targetAccounts = await control.modelDb.findAll(contact.class.PersonAccount, {
_id: { $in: department.members }
@ -199,9 +213,11 @@ export async function OnDepartmentRemove (tx: Tx, control: TriggerControl): Prom
})
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 }))
result.push(
control.txFactory.createTxMixin(em._id, em._class, em.space, hr.mixin.Staff, { department: undefined })
)
})
res.push(
result.push(
...getTxes(
control.txFactory,
targetAccounts.map((it) => it._id),
@ -209,57 +225,70 @@ export async function OnDepartmentRemove (tx: Tx, control: TriggerControl): Prom
removed.map((p) => p._id)
)
)
return res
}
return result
}
/**
* @public
*/
export async function OnEmployee (tx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnEmployee (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const ctx = TxProcessor.extractTx(tx) as TxMixin<Person, Employee>
const person = (await control.findAll(control.ctx, contact.class.Person, { _id: ctx.objectId }))[0]
if (person === undefined) {
return []
continue
}
const employee = control.hierarchy.as(person, ctx.mixin)
if (control.hierarchy.hasMixin(person, hr.mixin.Staff) || !employee.active) {
return []
continue
}
return [
result.push(
control.txFactory.createTxMixin(ctx.objectId, ctx.objectClass, ctx.objectSpace, hr.mixin.Staff, {
department: hr.ids.Head
})
]
)
}
return result
}
/**
* @public
*/
export async function OnEmployeeDeactivate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnEmployeeDeactivate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const actualTx = TxProcessor.extractTx(tx)
if (core.class.TxMixin !== actualTx._class) {
return []
continue
}
const ctx = actualTx as TxMixin<Person, Employee>
if (ctx.mixin !== contact.mixin.Employee || ctx.attributes.active !== false) {
return []
continue
}
const targetAccount = control.modelDb.getAccountByPersonId(ctx.objectId) as PersonAccount[]
if (targetAccount.length === 0) return []
if (targetAccount.length === 0) {
continue
}
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(
result.push(
...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<Tx[]> {
export async function OnRequestCreate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
for (const tx of txes) {
const ctx = TxProcessor.extractTx(tx) as TxCreateDoc<Request>
const sender = getPersonAccountById(ctx.modifiedBy, control)
if (sender === undefined) return []
if (sender === undefined) {
continue
}
const request = TxProcessor.createDoc2Doc(ctx)
await sendEmailNotifications(control, sender, request, request.department, hr.ids.CreateRequestNotification)
}
return []
}
/**
* @public
*/
export async function OnRequestUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnRequestUpdate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
for (const tx of txes) {
const ctx = TxProcessor.extractTx(tx) as TxUpdateDoc<Request>
const sender = getPersonAccountById(ctx.modifiedBy, control)
if (sender === undefined) return []
if (sender === undefined) {
continue
}
const request = (await control.findAll(control.ctx, hr.class.Request, { _id: ctx.objectId }))[0] as Request
if (request === undefined) return []
if (request === undefined) {
continue
}
await sendEmailNotifications(control, sender, request, request.department, hr.ids.UpdateRequestNotification)
}
return []
}
/**
* @public
*/
export async function OnRequestRemove (tx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnRequestRemove (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
for (const tx of txes) {
const ctx = TxProcessor.extractTx(tx) as TxCreateDoc<Request>
const sender = getPersonAccountById(ctx.modifiedBy, control)
if (sender === undefined) return []
if (sender === undefined) {
continue
}
const request = control.removedMap.get(ctx.objectId) as Request
if (request === undefined) return []
if (request === undefined) {
continue
}
await sendEmailNotifications(control, sender, request, request.department, hr.ids.RemoveRequestNotification)
}
return []
}
@ -400,13 +445,19 @@ export async function RequestTextPresenter (doc: Doc, control: TriggerControl):
/**
* @public
*/
export async function OnPublicHolidayCreate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnPublicHolidayCreate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const ctx = TxProcessor.extractTx(tx) as TxCreateDoc<PublicHoliday>
const sender = getPersonAccountById(ctx.modifiedBy, control)
if (sender === undefined) return []
if (sender === undefined) {
continue
}
const employee = await getEmployee(sender.person as Ref<Employee>, control)
if (employee === undefined) return []
if (employee === undefined) {
continue
}
const publicHoliday = TxProcessor.createDoc2Doc(ctx)
await sendEmailNotifications(
@ -416,7 +467,8 @@ export async function OnPublicHolidayCreate (tx: Tx, control: TriggerControl): P
publicHoliday.department,
hr.ids.CreatePublicHolidayNotification
)
return []
}
return result
}
/**

View File

@ -43,7 +43,9 @@ export async function leadTextPresenter (doc: Doc): Promise<string> {
/**
* @public
*/
export async function OnWorkspaceOwnerAdded (tx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnWorkspaceOwnerAdded (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
let ownerId: Ref<PersonAccount> | undefined
if (control.hierarchy.isDerived(tx._class, core.class.TxCreateDoc)) {
const createTx = tx as TxCreateDoc<PersonAccount>
@ -60,7 +62,7 @@ export async function OnWorkspaceOwnerAdded (tx: Tx, control: TriggerControl): P
}
if (ownerId === undefined) {
return []
continue
}
const targetFunnel = (
@ -70,7 +72,7 @@ export async function OnWorkspaceOwnerAdded (tx: Tx, control: TriggerControl): P
)[0]
if (targetFunnel === undefined) {
return []
continue
}
if (
@ -81,10 +83,11 @@ export async function OnWorkspaceOwnerAdded (tx: Tx, control: TriggerControl): P
const updTx = control.txFactory.createTxUpdateDoc(lead.class.Funnel, targetFunnel.space, targetFunnel._id, {
owners: [ownerId]
})
return [updTx]
result.push(updTx)
}
}
return []
return result
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type

View File

@ -44,12 +44,20 @@ import {
} from '@hcengineering/server-notification-resources'
import { workbenchId } from '@hcengineering/workbench'
export async function OnEmployee (tx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnEmployee (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const actualTx = TxProcessor.extractTx(tx) as TxMixin<Person, Employee>
if (actualTx._class !== core.class.TxMixin) return []
if (actualTx.mixin !== contact.mixin.Employee) return []
if (actualTx._class !== core.class.TxMixin) {
continue
}
if (actualTx.mixin !== contact.mixin.Employee) {
continue
}
const val = actualTx.attributes.active
if (val === undefined) return []
if (val === undefined) {
continue
}
if (val) {
const freeRoom = (await control.findAll(control.ctx, love.class.Office, { person: null }))[0]
if (freeRoom !== undefined) {
@ -62,14 +70,15 @@ export async function OnEmployee (tx: Tx, control: TriggerControl): Promise<Tx[]
} else {
const room = (await control.findAll(control.ctx, love.class.Office, { person: actualTx.objectId }))[0]
if (room !== undefined) {
return [
result.push(
control.txFactory.createTxUpdateDoc(room._class, room.space, room._id, {
person: null
})
]
)
}
}
return []
}
return result
}
async function createUserInfo (acc: Ref<Account>, control: TriggerControl): Promise<Tx[]> {
@ -124,13 +133,17 @@ async function removeUserInfo (acc: Ref<Account>, control: TriggerControl): Prom
return res
}
export async function OnUserStatus (tx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnUserStatus (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const actualTx = TxProcessor.extractTx(tx) as TxCUD<UserStatus>
if (actualTx.objectClass !== core.class.UserStatus) return []
if (actualTx.objectClass !== core.class.UserStatus) {
continue
}
if (actualTx._class === core.class.TxCreateDoc) {
const createTx = actualTx as TxCreateDoc<UserStatus>
const status = TxProcessor.createDoc2Doc(createTx)
return await createUserInfo(status.user, control)
result.push(...(await createUserInfo(status.user, control)))
} else if (actualTx._class === core.class.TxUpdateDoc) {
const updateTx = actualTx as TxUpdateDoc<UserStatus>
const val = updateTx.operations.online
@ -138,19 +151,14 @@ export async function OnUserStatus (tx: Tx, control: TriggerControl): Promise<Tx
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)
result.push(...(await createUserInfo(status.user, control)))
} else {
return await new Promise((resolve) => {
setTimeout(() => {
void removeUserInfo(status.user, control).then((res) => {
resolve(res)
})
}, 20000)
})
result.push(...(await removeUserInfo(status.user, control)))
}
}
}
return []
}
return result
}
async function roomJoinHandler (info: ParticipantInfo, control: TriggerControl): Promise<Tx[]> {
@ -228,51 +236,65 @@ async function setDefaultRoomAccess (info: ParticipantInfo, control: TriggerCont
return res
}
export async function OnParticipantInfo (tx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnParticipantInfo (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const actualTx = TxProcessor.extractTx(tx) as TxCUD<ParticipantInfo>
if (actualTx._class === core.class.TxCreateDoc) {
const info = TxProcessor.createDoc2Doc(actualTx as TxCreateDoc<ParticipantInfo>)
return await roomJoinHandler(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 (removedInfo === undefined) {
continue
}
result.push(...(await setDefaultRoomAccess(removedInfo, control)))
continue
}
if (actualTx._class === core.class.TxUpdateDoc) {
const newRoom = (actualTx as TxUpdateDoc<ParticipantInfo>).operations.room
if (newRoom === undefined) return []
if (newRoom === undefined) {
continue
}
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
if (info === undefined) {
continue
}
return []
result.push(...(await rejectJoinRequests(info, control)))
result.push(...(await setDefaultRoomAccess(info, control)))
result.push(...(await roomJoinHandler(info, control)))
}
}
return result
}
export async function OnKnock (tx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnKnock (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
for (const tx of txes) {
const actualTx = TxProcessor.extractTx(tx) as TxCreateDoc<JoinRequest>
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 []
if (from === undefined) {
continue
}
const type = await control.modelDb.findOne(notification.class.NotificationType, {
_id: love.ids.KnockNotification
})
if (type === undefined) return []
if (type === undefined) {
continue
}
const provider = await control.modelDb.findOne(notification.class.NotificationProvider, {
_id: notification.providers.PushNotificationProvider
})
if (provider === undefined) return []
if (provider === undefined) {
continue
}
const notificationControl = await getNotificationProviderControl(control.ctx, control)
for (const user of roomInfo.persons) {
@ -292,31 +314,40 @@ export async function OnKnock (tx: Tx, control: TriggerControl): Promise<Tx[]> {
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<Tx[]> {
export async function OnInvite (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
for (const tx of txes) {
const actualTx = TxProcessor.extractTx(tx) as TxCreateDoc<Invite>
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 []
if (target === undefined) {
continue
}
const userAcc = control.modelDb.getAccountByPersonId(target._id) as PersonAccount[]
if (userAcc.length === 0) return []
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
})
if (type === undefined) return []
if (type === undefined) {
continue
}
const provider = await control.modelDb.findOne(notification.class.NotificationProvider, {
_id: notification.providers.PushNotificationProvider
})
if (provider === undefined) return []
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]
@ -335,6 +366,7 @@ export async function OnInvite (tx: Tx, control: TriggerControl): Promise<Tx[]>
}
}
}
}
return []
}

View File

@ -331,7 +331,7 @@ export async function getDocCollaborators (
const collaborators = new Set<Ref<Account>>()
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<PersonAccount>], control)
const usersInfo = await ctx.with('get-user-info', {}, (ctx) =>
getUsersInfo(ctx, [...Array.from(targets), originTx.modifiedBy as Ref<PersonAccount>], 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<PersonAccount>[],
control,
tx,
originTx,
doc,
activityMessage,
{ isOwn: true, isSpace: false, shouldUpdateTimestamp: true }
)
const notificationTxes = await ctx.with('create-collabdocinfo', {}, (ctx) =>
createCollabDocInfo(ctx, collaborators as Ref<PersonAccount>[], 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<PersonAccount>[], control)
const infos = await ctx.with('get-user-info', {}, (ctx) =>
getUsersInfo(ctx, [...newCollabs, originTx.modifiedBy] as Ref<PersonAccount>[], 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<PersonAccount>[],
control,
actualTx,
tx,
doc,
activityMessages,
{ isOwn: false, isSpace: false, shouldUpdateTimestamp: true }
)
await ctx.with('create-collab-doc-info', {}, (ctx) =>
createCollabDocInfo(ctx, collaborators as Ref<PersonAccount>[], 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,11 +1485,8 @@ async function updateCollaboratorDoc (
}
res = res.concat(
await ctx.with(
'create-collab-docinfo',
{},
async (ctx) =>
await createCollabDocInfo(
await ctx.with('create-collab-docinfo', {}, (ctx) =>
createCollabDocInfo(
ctx,
collabsInfo.result as Ref<PersonAccount>[],
control,
@ -1537,10 +1500,8 @@ async function updateCollaboratorDoc (
)
)
} 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,12 +1533,16 @@ async function updateCollaboratorDoc (
/**
* @public
*/
export async function OnAttributeCreate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnAttributeCreate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const attribute = TxProcessor.createDoc2Doc(tx as TxCreateDoc<AnyAttribute>)
const group = (
await control.modelDb.findAll(notification.class.NotificationGroup, { objectClass: attribute.attributeOf })
)[0]
if (group === undefined) return []
if (group === undefined) {
continue
}
const isCollection: boolean = core.class.Collection === attribute.type._class
const objectClass = !isCollection ? attribute.attributeOf : (attribute.type as Collection<AttachedDoc>).of
const txClasses = !isCollection
@ -1606,22 +1569,34 @@ export async function OnAttributeCreate (tx: Tx, control: TriggerControl): Promi
}
const id =
`${notification.class.NotificationType}_${attribute.attributeOf}_${attribute.name}` as Ref<NotificationType>
const res = control.txFactory.createTxCreateDoc(notification.class.NotificationType, core.space.Model, data, id)
return [res]
result.push(control.txFactory.createTxCreateDoc(notification.class.NotificationType, core.space.Model, data, id))
}
return result
}
/**
* @public
*/
export async function OnAttributeUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnAttributeUpdate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const ctx = tx as TxUpdateDoc<AnyAttribute>
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, {
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 [res]
)
}
return result
}
async function applyUserTxes (
@ -1755,10 +1730,8 @@ export async function createCollaboratorNotifications (
): Promise<Tx[]> {
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<Doc>, cache)
return await ctx.with('updateDerivedCollaborators', {}, (ctx) =>
updateCollaborators(ctx, control, TxProcessor.extractTx(tx) as TxCUD<Doc>, 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<Doc>, control, activityMessages, originTx ?? tx, cache)
const res = await ctx.with('createCollaboratorDoc', {}, (ctx) =>
createCollaboratorDoc(ctx, tx as TxCreateDoc<Doc>, 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<Doc>, control, originTx ?? tx, activityMessages, cache)
let res = await ctx.with('updateCollaboratorDoc', {}, (ctx) =>
updateCollaboratorDoc(ctx, tx as TxUpdateDoc<Doc>, control, originTx ?? tx, activityMessages, cache)
)
res = res.concat(
await ctx.with(
'updateCollaboratorMixin',
{},
async (ctx) =>
await updateCollaboratorsMixin(
ctx,
tx as TxMixin<Doc, Collaborators>,
control,
activityMessages,
originTx ?? tx
)
await ctx.with('updateCollaboratorMixin', {}, (ctx) =>
updateCollaboratorsMixin(ctx, tx as TxMixin<Doc, Collaborators>, 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<Doc, AttachedDoc>, control, activityMessages, cache)
const res = await ctx.with('collectionCollabDoc', {}, (ctx) =>
collectionCollabDoc(ctx, tx as TxCollectionCUD<Doc, AttachedDoc>, 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<Doc>, control: TriggerControl): Promise<Tx[]> {
async function OnEmployeeDeactivate (txes: TxCUD<Doc>[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const actualTx = TxProcessor.extractTx(tx)
if (core.class.TxMixin !== actualTx._class) {
return []
continue
}
const ctx = actualTx as TxMixin<Person, Employee>
if (ctx.mixin !== contact.mixin.Employee || ctx.attributes.active !== false) {
return []
continue
}
const targetAccount = control.modelDb.getAccountByPersonId(ctx.objectId) as PersonAccount[]
if (targetAccount.length === 0) return []
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))
result.push(control.txFactory.createTxRemoveDoc(sub._class, sub.space, sub._id))
}
}
return res
}
return result
}
async function OnDocRemove (txes: TxCUD<Doc>[], control: TriggerControl): Promise<Tx[]> {

View File

@ -84,22 +84,23 @@ export async function applicationTextPresenter (doc: Doc, control: TriggerContro
/**
* @public
*/
export async function OnRecruitUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnRecruitUpdate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const actualTx = TxProcessor.extractTx(tx) as TxCUD<Doc>
if (!control.hierarchy.isDerived(actualTx.objectClass, recruit.class.Vacancy)) {
return []
continue
}
const res: Tx[] = []
if (actualTx._class === core.class.TxCreateDoc) {
handleVacancyCreate(control, actualTx, res)
handleVacancyCreate(control, actualTx, result)
} else if (actualTx._class === core.class.TxUpdateDoc) {
await handleVacancyUpdate(control, actualTx, res)
await handleVacancyUpdate(control, actualTx, result)
} else if (actualTx._class === core.class.TxRemoveDoc) {
handleVacancyRemove(control, actualTx, res)
handleVacancyRemove(control, actualTx, result)
}
return res
}
return result
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type

View File

@ -92,23 +92,31 @@ export async function getOwnerPosition (
/**
* @public
*/
export async function OnRoleNameUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnRoleNameUpdate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const actualTx = TxProcessor.extractTx(tx)
const updateTx = actualTx as TxUpdateDoc<Role>
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 []
if (roleAttribute === undefined) {
continue
}
const updAttrTx = control.txFactory.createTxUpdateDoc(core.class.Attribute, core.space.Model, roleAttribute._id, {
result.push(
control.txFactory.createTxUpdateDoc(core.class.Attribute, core.space.Model, roleAttribute._id, {
label: getEmbeddedLabel(updateTx.operations.name)
})
return [updAttrTx]
)
}
return result
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type

View File

@ -49,7 +49,7 @@ export async function TagElementRemove (
/**
* @public
*/
export async function onTagReference (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
export async function OnTagReference (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const actualTx = TxProcessor.extractTx(tx)
@ -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

View File

@ -75,9 +75,9 @@ export async function FindMessages (
/**
* @public
*/
export async function OnMessageCreate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = []
export async function OnMessageCreate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const message = TxProcessor.createDoc2Doc<TelegramMessage>(tx as TxCreateDoc<TelegramMessage>)
const channel = (
await control.findAll(control.ctx, contact.class.Channel, { _id: message.attachedTo }, { limit: 1 })
@ -87,11 +87,12 @@ export async function OnMessageCreate (tx: Tx, control: TriggerControl): Promise
const tx = control.txFactory.createTxUpdateDoc(channel._class, channel.space, channel._id, {
lastMessage: message.sendOn
})
res.push(tx)
result.push(tx)
}
}
}
return res
return result
}
/**

View File

@ -72,33 +72,57 @@ export async function OnTask (txes: Tx[], control: TriggerControl): Promise<Tx[]
return result
}
export async function OnWorkSlotUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnWorkSlotUpdate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const actualTx = TxProcessor.extractTx(tx) as TxCUD<WorkSlot>
if (!control.hierarchy.isDerived(actualTx.objectClass, time.class.WorkSlot)) return []
if (!control.hierarchy.isDerived(actualTx._class, core.class.TxUpdateDoc)) return []
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<WorkSlot>
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 []
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 })]
const workslot = (
await control.findAll(control.ctx, time.class.WorkSlot, { _id: updTx.objectId }, { limit: 1 })
)[0]
if (workslot === undefined) {
continue
}
return []
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 (tx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnWorkSlotCreate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
for (const tx of txes) {
const actualTx = TxProcessor.extractTx(tx) as TxCUD<WorkSlot>
if (!control.hierarchy.isDerived(actualTx.objectClass, time.class.WorkSlot)) return []
if (!control.hierarchy.isDerived(actualTx._class, core.class.TxCreateDoc)) return []
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<WorkSlot>)
const workslots = await control.findAll(control.ctx, time.class.WorkSlot, { attachedTo: workslot.attachedTo })
if (workslots.length > 1) return []
if (workslots.length > 1) {
continue
}
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 []
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<Issue> }))[0]
if (issue === undefined) return []
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]
@ -109,9 +133,11 @@ export async function OnWorkSlotCreate (tx: Tx, control: TriggerControl): Promis
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 === undefined) {
continue
}
if (current.category !== task.statusCategory.UnStarted && current.category !== task.statusCategory.ToDo) {
return []
continue
}
const nextStatus = typeStatuses.find((p) => p.category === task.statusCategory.Active)
if (nextStatus !== undefined) {
@ -127,7 +153,7 @@ export async function OnWorkSlotCreate (tx: Tx, control: TriggerControl): Promis
innerTx
)
await control.apply(control.ctx, [outerTx])
return []
}
}
}
}
@ -135,18 +161,31 @@ export async function OnWorkSlotCreate (tx: Tx, control: TriggerControl): Promis
return []
}
export async function OnToDoRemove (tx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnToDoRemove (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
for (const tx of txes) {
const actualTx = TxProcessor.extractTx(tx) as TxCUD<ToDo>
if (!control.hierarchy.isDerived(actualTx.objectClass, time.class.ToDo)) return []
if (!control.hierarchy.isDerived(actualTx._class, core.class.TxRemoveDoc)) return []
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) return []
if (todo === undefined) {
continue
}
// it was closed, do nothing
if (todo.doneOn != null) return []
if (todo.doneOn != null) {
continue
}
const todos = await control.findAll(control.ctx, time.class.ToDo, { attachedTo: todo.attachedTo })
if (todos.length > 0) return []
if (todos.length > 0) {
continue
}
const issue = (await control.findAll(control.ctx, tracker.class.Issue, { _id: todo.attachedTo as Ref<Issue> }))[0]
if (issue === undefined) return []
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]
@ -158,8 +197,12 @@ export async function OnToDoRemove (tx: Tx, control: TriggerControl): Promise<Tx
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 []
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, {
@ -173,7 +216,7 @@ export async function OnToDoRemove (tx: Tx, control: TriggerControl): Promise<Tx
innerTx
)
await control.apply(control.ctx, [outerTx])
return []
}
}
}
}
@ -181,12 +224,17 @@ export async function OnToDoRemove (tx: Tx, control: TriggerControl): Promise<Tx
return []
}
export async function OnToDoCreate (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
export async function OnToDoCreate (txes: TxCUD<Doc>[], control: TriggerControl): Promise<Tx[]> {
const hierarchy = control.hierarchy
for (const tx of txes) {
const createTx = TxProcessor.extractTx(tx) as TxCreateDoc<ToDo>
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<Class<Doc>>,
@ -194,18 +242,20 @@ export async function OnToDoCreate (tx: TxCUD<Doc>, control: TriggerControl): Pr
)
if (mixin === undefined) {
return []
continue
}
const todo = TxProcessor.createDoc2Doc(createTx)
const account = control.modelDb.getAccountByPersonId(todo.user) as PersonAccount[]
if (account.length === 0) {
return []
continue
}
const object = (await control.findAll(control.ctx, todo.attachedToClass, { _id: todo.attachedTo }))[0]
if (object === undefined) return []
if (object === undefined) {
continue
}
const person = (
await control.findAll(
@ -215,12 +265,16 @@ export async function OnToDoCreate (tx: TxCUD<Doc>, control: TriggerControl): Pr
{ limit: 1 }
)
)[0]
if (person === undefined) return []
if (person === undefined) {
continue
}
const personSpace = (
await control.findAll(control.ctx, contact.class.PersonSpace, { person: todo.user }, { limit: 1 })
)[0]
if (personSpace === undefined) return []
if (personSpace === undefined) {
continue
}
// TODO: Select a proper account
const receiverInfo: ReceiverInfo = {
@ -279,16 +333,23 @@ export async function OnToDoCreate (tx: TxCUD<Doc>, control: TriggerControl): Pr
return [receiverInfo.account.email]
}
}
}
return []
}
/**
* @public
*/
export async function OnToDoUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnToDoUpdate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const actualTx = TxProcessor.extractTx(tx) as TxCUD<ToDo>
if (!control.hierarchy.isDerived(actualTx.objectClass, time.class.ToDo)) return []
if (!control.hierarchy.isDerived(actualTx._class, core.class.TxUpdateDoc)) return []
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<ToDo>
const doneOn = updTx.operations.doneOn
const title = updTx.operations.title
@ -296,7 +357,6 @@ export async function OnToDoUpdate (tx: Tx, control: TriggerControl): Promise<Tx
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) {
@ -308,7 +368,7 @@ export async function OnToDoUpdate (tx: Tx, control: TriggerControl): Promise<Tx
event.collection,
innerTx
)
res.push(outerTx)
result.push(outerTx)
} else if (event.dueDate > doneOn) {
const upd: DocumentUpdate<WorkSlot> = {
dueDate: doneOn
@ -327,7 +387,7 @@ export async function OnToDoUpdate (tx: Tx, control: TriggerControl): Promise<Tx
event.collection,
innerTx
)
res.push(outerTx)
result.push(outerTx)
resEvents.push({
...event,
dueDate: doneOn
@ -337,7 +397,9 @@ export async function OnToDoUpdate (tx: Tx, control: TriggerControl): Promise<Tx
}
}
const todo = (await control.findAll(control.ctx, time.class.ToDo, { _id: updTx.objectId }))[0]
if (todo === undefined) return res
if (todo === undefined) {
continue
}
const funcs = control.hierarchy.classHierarchyMixin<Class<Doc>, OnToDo>(
todo.attachedToClass,
serverTime.mixin.OnToDo
@ -347,11 +409,10 @@ export async function OnToDoUpdate (tx: Tx, control: TriggerControl): Promise<Tx
const todoRes = await func(control, resEvents, todo)
await control.apply(control.ctx, todoRes)
}
return res
continue
}
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<WorkSlot> = {}
if (title !== undefined) {
@ -374,11 +435,11 @@ export async function OnToDoUpdate (tx: Tx, control: TriggerControl): Promise<Tx
event.collection,
innerTx
)
res.push(outerTx)
result.push(outerTx)
}
return res
}
return []
}
return result
}
/**

View File

@ -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<Issue>,
@ -160,30 +160,35 @@ export async function getIssueNotificationContent (
/**
* @public
*/
export async function OnComponentRemove (tx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnComponentRemove (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const ctx = TxProcessor.extractTx(tx) as TxRemoveDoc<Component>
const issues = await control.findAll(control.ctx, tracker.class.Issue, {
component: ctx.objectId
})
if (issues === undefined) return []
const res: Tx[] = []
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)
res.push(tx)
result.push(tx)
}
return res
}
return result
}
/**
* @public
*/
export async function OnWorkspaceOwnerAdded (tx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnWorkspaceOwnerAdded (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
let ownerId: Ref<PersonAccount> | undefined
if (control.hierarchy.isDerived(tx._class, core.class.TxCreateDoc)) {
const createTx = tx as TxCreateDoc<PersonAccount>
@ -200,7 +205,7 @@ export async function OnWorkspaceOwnerAdded (tx: Tx, control: TriggerControl): P
}
if (ownerId === undefined) {
return []
continue
}
const targetProject = (
@ -210,7 +215,7 @@ export async function OnWorkspaceOwnerAdded (tx: Tx, control: TriggerControl): P
)[0]
if (targetProject === undefined) {
return []
continue
}
if (
@ -218,19 +223,22 @@ export async function OnWorkspaceOwnerAdded (tx: Tx, control: TriggerControl): P
targetProject.owners.length === 0 ||
targetProject.owners[0] === core.account.System
) {
const updTx = control.txFactory.createTxUpdateDoc(tracker.class.Project, targetProject.space, targetProject._id, {
result.push(
control.txFactory.createTxUpdateDoc(tracker.class.Project, targetProject.space, targetProject._id, {
owners: [ownerId]
})
return [updTx]
)
}
return []
}
return result
}
/**
* @public
*/
export async function OnIssueUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnIssueUpdate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const actualTx = TxProcessor.extractTx(tx)
// Check TimeReport operations
@ -241,7 +249,7 @@ export async function OnIssueUpdate (tx: Tx, control: TriggerControl): Promise<T
) {
const cud = actualTx as TxCUD<TimeSpendReport>
if (cud.objectClass === tracker.class.TimeSpendReport) {
return await doTimeReportUpdate(cud, tx, control)
result.push(...(await doTimeReportUpdate(cud, tx, control)))
}
}
@ -249,17 +257,16 @@ export async function OnIssueUpdate (tx: Tx, control: TriggerControl): Promise<T
const createTx = actualTx as TxCreateDoc<Issue>
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
updateIssueParentEstimations(issue, result, control, [], issue.parents)
continue
}
}
if (actualTx._class === core.class.TxUpdateDoc) {
const updateTx = actualTx as TxUpdateDoc<Issue>
if (control.hierarchy.isDerived(updateTx.objectClass, tracker.class.Issue)) {
return await doIssueUpdate(updateTx, control, tx as TxCollectionCUD<Issue, AttachedDoc>)
result.push(...(await doIssueUpdate(updateTx, control, tx as TxCollectionCUD<Issue, AttachedDoc>)))
continue
}
}
if (actualTx._class === core.class.TxRemoveDoc) {
@ -268,7 +275,6 @@ export async function OnIssueUpdate (tx: Tx, control: TriggerControl): Promise<T
const parentIssue = await control.findAll(control.ctx, tracker.class.Issue, {
'childInfo.childId': removeTx.objectId
})
const res: Tx[] = []
const parents: IssueParentInfo[] = parentIssue.map((it) => ({
parentId: it._id,
parentTitle: it.title,
@ -282,15 +288,15 @@ export async function OnIssueUpdate (tx: Tx, control: TriggerControl): Promise<T
reportedTime: 0,
space: removeTx.space
},
res,
result,
control,
parents,
[]
)
return res
}
}
return []
}
return result
}
async function doTimeReportUpdate (cud: TxCUD<TimeSpendReport>, tx: Tx, control: TriggerControl): Promise<Tx[]> {

View File

@ -20,24 +20,30 @@ import view from '@hcengineering/view'
/**
* @public
*/
export async function OnCustomAttributeRemove (tx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnCustomAttributeRemove (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
const hierarchy = control.hierarchy
const ptx = tx as TxRemoveDoc<AnyAttribute>
if (!checkTx(ptx, hierarchy)) return []
if (!checkTx(ptx, hierarchy)) {
continue
}
const attribute = control.removedMap.get(ptx.objectId) as AnyAttribute
if (attribute === undefined) return []
if (attribute === undefined) {
continue
}
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 }
})
res.push(tx)
result.push(tx)
}
return res
}
return result
}
function checkTx (ptx: TxRemoveDoc<AnyAttribute>, hierarchy: Hierarchy): boolean {

View File

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

View File

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

View File

@ -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<Doc>[] = []
@ -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)

View File

@ -62,7 +62,7 @@ export class BlobClient {
if (i === 4) {
ctx.error('Failed to check file', { name, error: err })
}
await new Promise<void>((resolve) => setTimeout(resolve, 500))
await new Promise<void>((resolve) => setTimeout(resolve, 10))
}
}
return false
@ -189,7 +189,7 @@ export class BlobClient {
})
throw err
}
await new Promise<void>((resolve) => setTimeout(resolve, 1000))
await new Promise<void>((resolve) => setTimeout(resolve, 10))
// retry
}
}

View File

@ -42,14 +42,8 @@ async function loadCollaborativeDocVersion (
documentId: string,
versionId: string
): Promise<YDoc | undefined> {
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<void> {
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))
}
})
}

View File

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

View File

@ -27,8 +27,8 @@ export async function getContent (
): Promise<GetContentResponse> {
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 {

View File

@ -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<string, Uint8Array> = {}
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()
}

View File

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

View File

@ -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<void> {
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<YDoc | undefined> {
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<void> {
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<DocUpdateMessage> = {
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,

View File

@ -90,7 +90,7 @@ class BenchmarkDbAdapter extends DummyDbAdapter {
return toFindResult<T>(result as T[])
}
async tx (ctx: MeasureContext, ...tx: Tx[]): Promise<TxResult[]> {
tx (ctx: MeasureContext, ...tx: Tx[]): Promise<TxResult[]> {
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([{}])
}
}
/**

View File

@ -99,8 +99,8 @@ export class DbAdapterManagerImpl implements DBAdapterManager {
}
}
async initAdapters (ctx: MeasureContext): Promise<void> {
await ctx.with('init-adapters', {}, async (ctx) => {
initAdapters (ctx: MeasureContext): Promise<void> {
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) {

View File

@ -133,20 +133,20 @@ class InMemoryAdapter extends DummyDbAdapter implements DbAdapter {
this.modeldb = new ModelDb(hierarchy)
}
async findAll<T extends Doc>(
findAll<T extends Doc>(
ctx: MeasureContext,
_class: Ref<Class<T>>,
query: DocumentQuery<T>,
options?: FindOptions<T>
): Promise<FindResult<T>> {
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<Doc>[]): Promise<Doc[]> {
return await this.modeldb.findAll(core.class.Doc, { _id: { $in: docs } })
load (ctx: MeasureContext, domain: Domain, docs: Ref<Doc>[]): Promise<Doc[]> {
return this.modeldb.findAll(core.class.Doc, { _id: { $in: docs } })
}
async tx (ctx: MeasureContext, ...tx: Tx[]): Promise<TxResult[]> {
tx (ctx: MeasureContext, ...tx: Tx[]): Promise<TxResult[]> {
// 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)
}
}

View File

@ -92,37 +92,37 @@ class PipelineImpl implements Pipeline {
return current
}
async findAll<T extends Doc>(
findAll<T extends Doc>(
ctx: MeasureContext,
_class: Ref<Class<T>>,
query: DocumentQuery<T>,
options?: FindOptions<T>
): Promise<FindResult<T>> {
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<Tx[] | LoadModelResponse> {
return this.head !== undefined ? await this.head.loadModel(ctx, lastModelTx, hash) : []
loadModel (ctx: MeasureContext, lastModelTx: Timestamp, hash?: string): Promise<Tx[] | LoadModelResponse> {
return this.head !== undefined ? this.head.loadModel(ctx, lastModelTx, hash) : Promise.resolve([])
}
async groupBy<T, P extends Doc>(
groupBy<T, P extends Doc>(
ctx: MeasureContext,
domain: Domain,
field: string,
query?: DocumentQuery<P>
): Promise<Map<T, number>> {
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<SearchResult> {
return this.head !== undefined ? await this.head.searchFulltext(ctx, query, options) : { docs: [] }
searchFulltext (ctx: MeasureContext, query: SearchQuery, options: SearchOptions): Promise<SearchResult> {
return this.head !== undefined ? this.head.searchFulltext(ctx, query, options) : Promise.resolve({ docs: [] })
}
async tx (ctx: MeasureContext, tx: Tx[]): Promise<TxResult> {
tx (ctx: MeasureContext, tx: Tx[]): Promise<TxResult> {
if (this.head !== undefined) {
return await this.head.tx(ctx, tx)
return this.head.tx(ctx, tx)
}
return {}
return Promise.resolve({})
}
handleBroadcast (ctx: MeasureContext<SessionData>): Promise<void> {

View File

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

View File

@ -41,7 +41,7 @@ export class BackupClientOps {
idIndex = 0
chunkInfo = new Map<number, ChunkInfo>()
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<void> {
await ctx.with('close-chunk', {}, async () => {
closeChunk (ctx: MeasureContext, idx: number): Promise<void> {
return ctx.with('close-chunk', {}, async () => {
const chunk = this.chunkInfo.get(idx)
this.chunkInfo.delete(idx)
if (chunk != null) {

View File

@ -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<Tx>
trigger: { op: TriggerFunc | Promise<TriggerFunc>, resource: Resource<TriggerFunc>, 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<void> {
tresolve = Promise.resolve()
tx (txes: Tx[]): Promise<void> {
for (const tx of txes) {
if (tx._class === core.class.TxCreateDoc) {
const createTx = tx as TxCreateDoc<Doc>
@ -82,13 +81,14 @@ export class Triggers {
}
}
}
return this.tresolve
}
async applyTrigger (
ctx: MeasureContext,
ctrl: Omit<TriggerControl, 'txFactory'>,
matches: Tx[],
{ trigger, arrays }: TriggerRecord
{ trigger }: TriggerRecord
): Promise<Tx[]> {
const result: Tx[] = []
const apply: Tx[] = []
@ -98,31 +98,28 @@ 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)
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, tx, err })
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<Tx[]> {
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 }
)
}
}

View File

@ -260,7 +260,7 @@ export interface TriggerControl {
/**
* @public
*/
export type TriggerFunc = (tx: Tx | Tx[], ctrl: TriggerControl) => Promise<Tx[]>
export type TriggerFunc = (tx: Tx[], ctrl: TriggerControl) => Promise<Tx[]>
/**
* @public
@ -273,9 +273,6 @@ export interface Trigger extends Doc {
// We should match transaction
txMatch?: DocumentQuery<Tx>
// 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<Pipeline>
tickHash: number
tickHandlers: Map<string, TickHandler>
sessions: Map<string, { session: Session, socket: ConnectionSocket, tickHash: number }>
upgrade: boolean

View File

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

View File

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

View File

@ -62,7 +62,7 @@ class ElasticAdapter implements FullTextAdapter {
async initMapping (ctx: MeasureContext): Promise<boolean> {
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}_*`]
})

View File

@ -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,11 +429,8 @@ 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, () =>
blobInfo = await ctx.with('resize', {}, (ctx) =>
getGeneratePreview(ctx, blobInfo as PlatformBlob, size, uuid, config, payload, accept, () =>
join(tempFileDir, `${++temoFileIndex}`)
)
)
@ -443,16 +438,14 @@ export function start (
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.

View File

@ -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<DocIndexState> | WithLookup<DocIndexState>[] = 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

View File

@ -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<SessionData>, txes)
return this.provideTx(ctx as MeasureContext<SessionData>, txes)
}
)
updateOperationLog(opLogMetrics, op)

View File

@ -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<SessionData>): Promise<void> {
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<void> {
sendWarmup (): void {
if (!this.warmupTriggered) {
this.warmupTriggered = true
setTimeout(() => {
this.warmupTriggered = false
void this._sendWarmup()
void this._sendWarmup().catch(() => {})
}, 25)
}
}

View File

@ -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<Doc>)?.ctx ?? metrics
delete (options as ServerFindOptions<Doc>)?.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

View File

@ -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<Doc>[]): Promise<void> {
await adapterManager.getAdapter(domain, true).clean(ctx, domain, docs)
clean (ctx: MeasureContext, domain: Domain, docs: Ref<Doc>[]): Promise<void> {
return adapterManager.getAdapter(domain, true).clean(ctx, domain, docs)
},
groupBy<T, P extends Doc>(
ctx: MeasureContext,

View File

@ -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<T extends Doc>(ctx: MeasureContext, lookup: LookupData<T>): Promise<void> {
filterLookup<T extends Doc>(ctx: MeasureContext, lookup: LookupData<T>): void {
for (const key in lookup) {
const val = lookup[key]
if (Array.isArray(val)) {

View File

@ -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<SessionData>, txes: Tx[]): Promise<void> {
const _findAll: SessionFindAll = async (ctx, _class, query, options) => {
const findAll: SessionFindAll = async (ctx, _class, query, options) => {
const _ctx: MeasureContext = (options as ServerFindOptions<Doc>)?.ctx ?? ctx
delete (options as ServerFindOptions<Doc>)?.ctx
if (_ctx.contextData !== undefined) {
@ -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<D extends Doc>(
private getCollectionUpdateTx<D extends Doc>(
_id: Ref<D>,
_class: Ref<Class<D>>,
modifiedBy: Ref<Account>,
modifiedOn: number,
attachedTo: Pick<Doc, '_class' | 'space'>,
update: DocumentUpdate<D>
): Promise<Tx> {
): 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))

View File

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

View File

@ -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<Space>, {
name: `my-task-${i}`,

View File

@ -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<T extends Doc>(domain: Domain, query: DocumentQuery<T>): Promise<void> {
await this.db.collection(domain).deleteMany(this.translateRawQuery(query))
rawDeleteMany<T extends Doc>(domain: Domain, query: DocumentQuery<T>): Promise<void> {
return this.db.collection(domain).deleteMany(this.translateRawQuery(query)).then()
}
abstract init (): Promise<void>
@ -280,8 +278,9 @@ abstract class MongoAdapterBase implements DbAdapter {
return []
}
async close (): Promise<void> {
close (): Promise<void> {
this.client.close()
return Promise.resolve()
}
private translateQuery<T extends Doc>(
@ -460,13 +459,13 @@ abstract class MongoAdapterBase implements DbAdapter {
return result
}
private async fillLookup<T extends Doc>(
private fillLookup<T extends Doc>(
_class: Ref<Class<T>>,
object: any,
key: string,
fullKey: string,
targetObject: any
): Promise<void> {
): 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<T extends Doc>(
private fillLookupValue<T extends Doc>(
ctx: MeasureContext,
clazz: Ref<Class<T>>,
lookup: Lookup<T> | undefined,
@ -496,11 +495,11 @@ abstract class MongoAdapterBase implements DbAdapter {
field: string
domain: Domain
}
): Promise<void> {
): 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<T extends Doc>(
private fillReverseLookup<T extends Doc>(
clazz: Ref<Class<T>>,
lookup: ReverseLookups,
object: any,
parent?: string,
parentObject?: any
): Promise<void> {
): 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<T extends Doc>(
private fillSortPipeline<T extends Doc>(
clazz: Ref<Class<T>>,
options: FindOptions<T> | undefined,
pipeline: any[]
): Promise<void> {
): 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<T>[] = []
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<T, D extends Doc = Doc>(
groupBy<T, D extends Doc = Doc>(
ctx: MeasureContext,
domain: Domain,
field: string,
query?: DocumentQuery<D>
): Promise<Map<T, number>> {
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<T extends Doc>(
findAll<T extends Doc>(
ctx: MeasureContext,
_class: Ref<Class<T>>,
query: DocumentQuery<T>,
@ -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<T>
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,11 +1022,8 @@ abstract class MongoAdapterBase implements DbAdapter {
const flush = async (flush = false): Promise<void> => {
if (bulkUpdate.size > 1000 || flush) {
if (bulkUpdate.size > 0) {
await ctx.with(
'bulk-write-find',
{},
async () =>
await coll.bulkWrite(
await ctx.with('bulk-write-find', {}, () =>
coll.bulkWrite(
Array.from(bulkUpdate.entries()).map((it) => ({
updateOne: {
filter: { _id: it[0], '%hash%': null },
@ -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<Doc>[]): Promise<Doc[]> {
return await ctx.with('load', { domain }, async () => {
load (ctx: MeasureContext, domain: Domain, docs: Ref<Doc>[]): Promise<Doc[]> {
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<void> {
await ctx.with('upload', { domain }, async () => {
upload (ctx: MeasureContext, domain: Domain, docs: Doc[]): Promise<void> {
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<Ref<Doc>, DocumentUpdate<Doc>>): Promise<void> {
await ctx.with('update', { domain }, async () => {
update (ctx: MeasureContext, domain: Domain, operations: Map<Ref<Doc>, DocumentUpdate<Doc>>): Promise<void> {
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<Doc>[]): Promise<void> {
await ctx.with('clean', {}, async () => {
clean (ctx: MeasureContext, domain: Domain, docs: Ref<Doc>[]): Promise<void> {
return ctx.with('clean', {}, async () => {
if (docs.length > 0) {
await this.db.collection<Doc>(domain).deleteMany({ _id: { $in: docs } })
}

View File

@ -402,13 +402,13 @@ abstract class PostgresAdapterBase implements DbAdapter {
}
}
async findAll<T extends Doc>(
findAll<T extends Doc>(
ctx: MeasureContext<SessionData>,
_class: Ref<Class<T>>,
query: DocumentQuery<T>,
options?: ServerFindOptions<T>
): Promise<FindResult<T>> {
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<void> => {
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<Doc>[]): Promise<Doc[]> {
return await ctx.with('load', { domain }, async () => {
load (ctx: MeasureContext, domain: Domain, docs: Ref<Doc>[]): Promise<Doc[]> {
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<void> {
await ctx.with('upload', { domain }, async (ctx) => {
upload (ctx: MeasureContext, domain: Domain, docs: Doc[]): Promise<void> {
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<T, P extends Doc>(
groupBy<T, P extends Doc>(
ctx: MeasureContext,
domain: Domain,
field: string,
query?: DocumentQuery<P>
): Promise<Map<T, number>> {
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<Ref<Doc>, DocumentUpdate<Doc>>): Promise<void> {
update (ctx: MeasureContext, domain: Domain, operations: Map<Ref<Doc>, DocumentUpdate<Doc>>): Promise<void> {
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<Doc>): Promise<TxResult> {
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<Doc>): Promise<TxResult> {
return await ctx.with('tx-update-doc', { _class: tx.objectClass }, async (_ctx) => {
protected txUpdateDoc (ctx: MeasureContext, tx: TxUpdateDoc<Doc>): Promise<TxResult> {
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<T extends Doc>(
ctx: MeasureContext,
tx: TxUpdateDoc<T>,
retrieve: boolean
): Promise<TxResult> {
return await ctx.with('update jsonb_set', {}, async (_ctx) => {
private updateDoc<T extends Doc>(ctx: MeasureContext, tx: TxUpdateDoc<T>, retrieve: boolean): Promise<TxResult> {
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<Class<Doc>>,
_id: Ref<Doc>,
forUpdate: boolean = false
): Promise<Doc | undefined> {
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 {}

View File

@ -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<UploadedObjectInfo> {
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 () => {

View File

@ -90,7 +90,7 @@ class TSessionManager implements SessionManager {
checkInterval: any
sessions = new Map<string, { session: Session, socket: ConnectionSocket }>()
reconnectIds = new Map<string, any>()
reconnectIds = new Set<string>()
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) {
try {
// update account lastVisit every minute per every workspace.∏
void this.getWorkspaceInfo(this.ctx, workspace.token)
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(() => {
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)
}
}
}, this.timeouts.reconnectTimeout)
void this.trySetStatus(workspace.context, sessionRef.session, false, workspace.workspaceId).catch(
() => {}
)
}
}
}
})
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 {
.finally(() => {
userCtx.end()
service.requests.delete(reqId)
}
})
}
}

View File

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

View File

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

View File

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

View File

@ -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<DocSyncInfo>(
(ctx) =>
this._client.findAll<DocSyncInfo>(
github.class.DocSyncInfo,
{
needSync: { $ne: githubSyncVersion },

View File

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