// // Copyright © 2025 Hardcore Engineering Inc. // // Licensed under the Eclipse Public License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. You may // obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // // See the License for the specific language governing permissions and // limitations under the License. // import cardPlugin, { Card } from '@hcengineering/card' import core, { ArrOf, Doc, RefTo, Tx, TxCreateDoc, TxCUD, TxMixin, TxProcessor, TxRemoveDoc, TxUpdateDoc } from '@hcengineering/core' import process, { ContextId, Execution, Method, parseContext, Process, ProcessContext, ProcessToDo, SelectedExecutionContext, State, Step, Transition, isUpdateTx, ProcessCustomEvent } from '@hcengineering/process' import { QueueTopic, TriggerControl } from '@hcengineering/server-core' import { ProcessMessage } from '@hcengineering/server-process' import { Absolute, Add, All, Append, Ceil, CurrentDate, CurrentUser, Cut, Divide, FirstValue, FirstWorkingDayAfter, Floor, Insert, LastValue, LowerCase, Modulo, Multiply, Offset, Power, Sqrt, Prepend, Random, Remove, RemoveFirst, RemoveLast, Replace, ReplaceAll, RoleContext, Round, Split, Subtract, Trim, UpperCase, EmptyArray, ExecutionInitiator, ExecutionStarted, FirstMatchValue, Filter } from './transform' import { RunSubProcess, CreateToDo, UpdateCard, CreateCard, AddRelation, AddTag, CheckToDoDone, CheckToDoCancelled, MatchCardCheck, CheckSubProcessesDone, CheckSubProcessMatch, CheckTime, FieldChangedCheck, EventCheck } from './functions' import { ToDoCancellRollback, ToDoCloseRollback } from './rollback' async function putEventToQueue (value: Omit, control: TriggerControl): Promise { if (control.queue === undefined) return const producer = control.queue.getProducer(control.ctx.newChild('queue', {}), QueueTopic.Process) try { await producer.send(control.ctx, control.workspace.uuid, [ { ...value, account: control.txFactory.account } ]) } catch (err) { control.ctx.error('Could not queue process event', { err, value }) } } export async function OnProcessToDoClose (txes: Tx[], control: TriggerControl): Promise { const res: Tx[] = [] for (const tx of txes) { if (tx._class !== core.class.TxUpdateDoc) continue const updateTx = tx as TxUpdateDoc if (!control.hierarchy.isDerived(updateTx.objectClass, process.class.ProcessToDo)) continue if (updateTx.operations.doneOn == null) continue const todo = ( await control.findAll(control.ctx, process.class.ProcessToDo, { _id: updateTx.objectId }, { limit: 1 }) )[0] if (todo === undefined) continue await putEventToQueue( { event: process.trigger.OnToDoClose, execution: todo.execution, createdOn: tx.modifiedOn, context: { todo } }, control ) } return res } export async function OnCustomEvent (txes: Tx[], control: TriggerControl): Promise { for (const tx of txes) { if (tx._class !== core.class.TxCreateDoc) continue const createTx = tx as TxCreateDoc if (!control.hierarchy.isDerived(createTx.objectClass, process.class.ProcessCustomEvent)) continue const customEvent = TxProcessor.createDoc2Doc(createTx) const card = await control.findAll(control.ctx, cardPlugin.class.Card, { _id: customEvent.card }, { limit: 1 }) if (card.length === 0) continue await putEventToQueue( { event: process.trigger.OnEvent, execution: customEvent.execution, createdOn: tx.modifiedOn, card: customEvent.card, context: { eventType: customEvent.eventType, card } }, control ) } return [] } export async function OnExecutionCreate (txes: Tx[], control: TriggerControl): Promise { for (const tx of txes) { if (tx._class !== core.class.TxCreateDoc) continue const createTx = tx as TxCreateDoc if (!control.hierarchy.isDerived(createTx.objectClass, process.class.Execution)) continue const execution = TxProcessor.createDoc2Doc(createTx) await putEventToQueue( { event: process.trigger.OnExecutionStart, execution: execution._id, createdOn: tx.modifiedOn, context: {} }, control ) } return [] } export async function OnProcessToDoRemove (txes: Tx[], control: TriggerControl): Promise { for (const tx of txes) { if (tx._class !== core.class.TxRemoveDoc) continue const removeTx = tx as TxRemoveDoc if (!control.hierarchy.isDerived(removeTx.objectClass, process.class.ProcessToDo)) continue const removedTodo = control.removedMap.get(removeTx.objectId) as ProcessToDo if (removedTodo === undefined) continue await putEventToQueue( { event: process.trigger.OnToDoRemove, execution: removedTodo.execution, createdOn: tx.modifiedOn, context: { todo: removedTodo } }, control ) } return [] } export async function OnExecutionContinue (txes: Tx[], control: TriggerControl): Promise { for (const tx of txes) { if (tx._class !== core.class.TxUpdateDoc) continue if (tx.space !== core.space.Tx) continue const updateTx = tx as TxUpdateDoc if (!control.hierarchy.isDerived(updateTx.objectClass, process.class.Execution)) continue const execution = ( await control.findAll(control.ctx, process.class.Execution, { _id: updateTx.objectId }, { limit: 1 }) )[0] if (execution === undefined) continue const error = execution.error if (error == null) continue const transition = execution.error?.[0].transition if (transition === undefined) continue await putEventToQueue( { event: process.trigger.OnExecutionContinue, execution: execution._id, createdOn: tx.modifiedOn, context: {} }, control ) } return [] } export async function OnProcessRemove (txes: Tx[], control: TriggerControl): Promise { const res: Tx[] = [] for (const tx of txes) { if (tx._class !== core.class.TxRemoveDoc) continue const removeTx = tx as TxRemoveDoc if (!control.hierarchy.isDerived(removeTx.objectClass, process.class.Process)) continue const transition = control.modelDb.findAllSync(process.class.Transition, { process: removeTx.objectId }) for (const tr of transition) { res.push(control.txFactory.createTxRemoveDoc(tr._class, tr.space, tr._id)) } const states = control.modelDb.findAllSync(process.class.State, { process: removeTx.objectId }) for (const st of states) { res.push(control.txFactory.createTxRemoveDoc(st._class, st.space, st._id)) } } return res } export async function OnStateRemove (txes: Tx[], control: TriggerControl): Promise { const res: Tx[] = [] for (const tx of txes) { if (tx._class !== core.class.TxRemoveDoc) continue const removeTx = tx as TxRemoveDoc if (!control.hierarchy.isDerived(removeTx.objectClass, process.class.State)) continue const transitions = control.modelDb.findAllSync(process.class.Transition, { to: removeTx.objectId }) for (const tr of transitions) { res.push(control.txFactory.createTxRemoveDoc(tr._class, tr.space, tr._id)) } const state = control.removedMap.get(removeTx.objectId) as State if (state === undefined) continue const _process = control.modelDb.findObject(state.process) if (_process === undefined) continue const syncTx = await syncContext(control, _process) if (syncTx !== undefined) { res.push(syncTx) } } return res } export async function OnExecutionRemove (txes: Tx[], control: TriggerControl): Promise { const res: Tx[] = [] for (const tx of txes) { if (tx._class !== core.class.TxRemoveDoc) continue const cudTx = tx as TxRemoveDoc if (!control.hierarchy.isDerived(cudTx.objectClass, process.class.Execution)) continue const todos = await control.findAll(control.ctx, process.class.ProcessToDo, { execution: cudTx.objectId, doneOn: null }) for (const todo of todos) { res.push(control.txFactory.createTxRemoveDoc(todo._class, todo.space, todo._id)) } } return res } async function getExecutionReassignTxes (card: Card, control: TriggerControl): Promise { const res: Tx[] = [] const cards = await control.findAll(control.ctx, cardPlugin.class.Card, { baseId: card.baseId }) const ids = cards.map((p) => p._id).filter((p) => p !== card._id) const executions = await control.findAll(control.ctx, process.class.Execution, { card: { $in: ids } }) for (const execution of executions) { res.push( control.txFactory.createTxUpdateDoc(execution._class, execution.space, execution._id, { card: card._id }) ) } return res } export async function OnCardCreate (txes: Tx[], control: TriggerControl): Promise { const res: Tx[] = [] for (const tx of txes) { if (tx._class !== core.class.TxCreateDoc) continue const createTx = tx as TxCreateDoc if (!control.hierarchy.isDerived(createTx.objectClass, cardPlugin.class.Card)) continue const obj = TxProcessor.createDoc2Doc(createTx) if (obj.baseId !== obj._id) { const reassignTxes = await getExecutionReassignTxes(obj, control) res.push(...reassignTxes) } } return res } export async function OnTransition (txes: Tx[], control: TriggerControl): Promise { const res: Tx[] = [] for (const tx of txes) { if (!control.hierarchy.isDerived(tx._class, core.class.TxCUD)) continue const cudTx = tx as TxCUD if (!control.hierarchy.isDerived(cudTx.objectClass, process.class.Transition)) continue const transition = (control.removedMap.get(cudTx.objectId) as Transition) ?? control.modelDb.findObject(cudTx.objectId) if (transition === undefined) continue const _process = control.modelDb.findObject(transition.process) if (_process === undefined) continue const syncTx = await syncContext(control, _process) if (syncTx !== undefined) { res.push(syncTx) } } return res } export async function OnCardUpdate (txes: Tx[], control: TriggerControl): Promise { const res: Tx[] = [] for (const tx of txes) { if (!control.hierarchy.isDerived(tx._class, core.class.TxCUD)) continue if (tx._class !== core.class.TxUpdateDoc && tx._class !== core.class.TxMixin) continue const cudTx = tx as TxUpdateDoc | TxMixin if (!control.hierarchy.isDerived(cudTx.objectClass, cardPlugin.class.Card)) continue const card = await control.findAll(control.ctx, cardPlugin.class.Card, { _id: cudTx.objectId }, { limit: 1 }) if (card.length === 0) continue const ops = isUpdateTx(cudTx) ? cudTx.operations : cudTx.attributes await putEventToQueue( { event: process.trigger.OnCardUpdate, card: cudTx.objectId, createdOn: tx.modifiedOn, context: { card: card[0], operations: ops ?? {} } }, control ) await putEventToQueue( { event: process.trigger.WhenFieldChanges, card: cudTx.objectId, createdOn: tx.modifiedOn, context: { card: card[0], operations: ops ?? {} } }, control ) } return res } function getName (current: ProcessContext | undefined, method: Method, action: Step): string { const nameField = method.createdContext?.nameField if (nameField !== undefined) { const name = action.params[nameField] if (name !== undefined && typeof name === 'string' && name !== '' && parseContext(name) === undefined) return name } return current?.name ?? '' } async function syncContext (control: TriggerControl, _process: Process): Promise { const transitions = control.modelDb.findAllSync(process.class.Transition, { process: _process._id }) const exists = new Set() let changed = false let index = 1 for (const transition of transitions) { for (const action of transition.actions) { if (action.context != null) { exists.add(action.context._id) const method = control.modelDb.findObject(action.methodId) const current = _process.context[action.context._id] if (method?.createdContext != null) { changed = true const ctx: SelectedExecutionContext = { type: 'context', id: action.context._id, key: '' } _process.context[action.context._id] = { name: getName(current, method, action), _class: action.context._class ?? method.createdContext._class, action: action._id, index: index++, producer: transition._id, value: ctx } } } if (action.results != null) { for (const result of action.results) { exists.add(result._id) changed = true const ctx: SelectedExecutionContext = { type: 'context', id: result._id, key: '' } const parentType = result.type._class === core.class.ArrOf ? (result.type as ArrOf).of : result.type const _class = parentType._class === core.class.RefTo ? (parentType as RefTo).to : parentType._class _process.context[result._id] = { name: result.name, isResult: true, type: result.type, action: action._id, _class, index: index++, producer: transition._id, value: ctx } } } } } const newContext: Record = {} for (const key of Object.keys(_process.context) as ContextId[]) { if (exists.has(key)) { newContext[key] = _process.context[key] continue } changed = true } if (changed) { return control.txFactory.createTxUpdateDoc(_process._class, _process.space, _process._id, { context: newContext }) } } export * from './utils' // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export default async () => ({ func: { RunSubProcess, CreateToDo, UpdateCard, CreateCard, AddRelation, AddTag, CheckToDoDone, CheckToDoCancelled, FieldChangedCheck, MatchCardCheck, CheckSubProcessesDone, CheckSubProcessMatch, CheckTime, EventCheck }, transform: { CurrentDate, CurrentUser, FirstValue, LastValue, Random, All, UpperCase, LowerCase, Trim, Prepend, Append, Replace, ReplaceAll, Split, Cut, Add, Subtract, Multiply, Divide, Modulo, Power, Sqrt, Round, Absolute, Ceil, Floor, Offset, FirstWorkingDayAfter, RoleContext, Insert, Remove, RemoveFirst, RemoveLast, EmptyArray, ExecutionInitiator, ExecutionStarted, FirstMatchValue, Filter }, rollbacks: { ToDoCloseRollback, ToDoCancellRollback }, trigger: { OnProcessRemove, OnStateRemove, OnTransition, OnCardUpdate, OnExecutionCreate, OnProcessToDoClose, OnProcessToDoRemove, OnExecutionContinue, OnCustomEvent, OnExecutionRemove, OnCardCreate } })