Rollback and custom events preparation (#10298)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov
2025-12-13 18:52:24 +05:00
committed by GitHub
parent 6d1bda5d54
commit 87db5a52be
14 changed files with 297 additions and 62 deletions
-12
View File
@@ -48,18 +48,6 @@ export function defineMethods (builder: Builder): void {
process.method.CreateToDo
)
builder.createDoc(
process.class.Trigger,
core.space.Model,
{
label: process.string.OnExecutionStart,
icon: process.icon.Process,
init: true,
requiredParams: []
},
process.trigger.OnExecutionStart
)
builder.createDoc(
process.class.Method,
core.space.Model,
+33 -4
View File
@@ -16,6 +16,7 @@ import core, {
AccountRole,
type Class,
DOMAIN_MODEL,
DOMAIN_TRANSIENT,
type Doc,
type Domain,
type Rank,
@@ -48,6 +49,7 @@ import {
type CheckFunc,
type ContextId,
type CreatedContext,
type ProcessCustomEvent,
type Execution,
type ExecutionContext,
type ExecutionError,
@@ -65,16 +67,17 @@ import {
type Transition,
type Trigger,
type UpdateCriteriaComponent,
processId
processId,
type EventButton
} from '@hcengineering/process'
import time from '@hcengineering/time'
import { type AnyComponent } from '@hcengineering/ui'
import { type AttributeCategory } from '@hcengineering/view'
import { defineMethods } from './actions'
import { defineTriggers } from './triggers'
import { defineFunctions } from './functions'
import process from './plugin'
import { definePermissions } from './permission'
import process from './plugin'
import { defineTriggers } from './triggers'
const DOMAIN_PROCESS = 'process' as Domain
const DOMAIN_PROCESS_LOG = 'process-log' as Domain
@@ -234,6 +237,30 @@ export class TState extends TDoc implements State {
rank!: Rank
}
@Model(process.class.ProcessCustomEvent, core.class.Doc, DOMAIN_TRANSIENT)
export class TProcessCustomEvent extends TDoc implements ProcessCustomEvent {
eventType!: string
@Prop(TypeRef(process.class.Execution), process.string.Execution)
execution!: Ref<Execution>
@Prop(TypeRef(card.class.Card), card.string.Card)
card!: Ref<Card>
}
@Model(process.class.EventButton, core.class.Doc, DOMAIN_PROCESS)
export class TEventButton extends TDoc implements EventButton {
title!: string
eventType!: string
@Prop(TypeRef(process.class.Execution), process.string.Execution)
execution!: Ref<Execution>
@Prop(TypeRef(card.class.Card), card.string.Card)
card!: Ref<Card>
}
@Model(process.class.ProcessFunction, core.class.Doc, DOMAIN_MODEL)
export class TProcessFunction extends TDoc implements ProcessFunction {
of!: Ref<Class<Doc<Space>>>
@@ -269,7 +296,9 @@ export function createModel (builder: Builder): void {
TTransition,
TTrigger,
TExecutionLog,
TUpdateCriteriaComponent
TUpdateCriteriaComponent,
TProcessCustomEvent,
TEventButton
)
builder.createDoc(
+30
View File
@@ -32,6 +32,18 @@ export function defineTriggers (builder: Builder): void {
process.trigger.OnToDoClose
)
builder.createDoc(
process.class.Trigger,
core.space.Model,
{
label: process.string.OnExecutionStart,
icon: process.icon.Process,
init: true,
requiredParams: []
},
process.trigger.OnExecutionStart
)
builder.createDoc(
process.class.Trigger,
core.space.Model,
@@ -80,6 +92,24 @@ export function defineTriggers (builder: Builder): void {
process.trigger.WhenFieldChanges
)
// builder.createDoc(
// process.class.Trigger,
// core.space.Model,
// {
// label: process.string.OnEvent,
// icon: process.icon.OnEvent,
// editor: process.component.OnEventEditor,
// presenter: process.component.OnEventPresenter,
// requiredParams: [
// 'eventType'
// ],
// checkFunction: process.triggerCheck.OnEventCheck,
// init: false,
// auto: false
// },
// process.trigger.OnEvent
// )
builder.createDoc(
process.class.Trigger,
core.space.Model,
+27
View File
@@ -61,14 +61,25 @@ export function createModel (builder: Builder): void {
})
builder.mixin(process.trigger.OnCardUpdate, process.class.Trigger, serverProcess.mixin.TriggerImpl, {
preventRollback: true,
serverCheckFunc: serverProcess.func.MatchCardCheck
})
builder.mixin(process.trigger.WhenFieldChanges, process.class.Trigger, serverProcess.mixin.TriggerImpl, {
preventRollback: true,
serverCheckFunc: serverProcess.func.FieldChangedCheck
})
builder.mixin(process.trigger.OnExecutionStart, process.class.Trigger, serverProcess.mixin.TriggerImpl, {
preventRollback: true
})
// builder.mixin(process.trigger.OnEvent, process.class.Trigger, serverProcess.mixin.TriggerImpl, {
// serverCheckFunc: serverProcess.func.EventCheck
// })
builder.mixin(process.trigger.OnTime, process.class.Trigger, serverProcess.mixin.TriggerImpl, {
preventRollback: true,
serverCheckFunc: serverProcess.func.CheckTime
})
@@ -290,6 +301,22 @@ export function createModel (builder: Builder): void {
isAsync: true
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverProcess.trigger.OnCustomEvent,
txMatch: {
_class: core.class.TxCreateDoc,
objectClass: process.class.ProcessCustomEvent
}
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverProcess.trigger.OnExecutionRemove,
txMatch: {
_class: core.class.TxRemoveDoc,
objectClass: process.class.Execution
}
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverProcess.trigger.OnProcessToDoClose,
txMatch: {
+2 -1
View File
@@ -27,5 +27,6 @@ loadMetadata(process.icon, {
Start: `${icons}#start`,
WhenCardMatches: `${icons}#card-matches`,
WhenFieldChanges: `${icons}#field-change`,
Time: `${icons}#time`
Time: `${icons}#time`,
OnEvent: `${icons}#start`
})
@@ -17,7 +17,7 @@
import { getCurrentEmployee } from '@hcengineering/contact'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Execution, ExecutionStatus, ProcessToDo } from '@hcengineering/process'
import { EventButton, Execution, ExecutionStatus, ProcessToDo } from '@hcengineering/process'
import { Button } from '@hcengineering/ui'
import process from '../plugin'
@@ -25,6 +25,18 @@
let docs: Execution[] = []
let todos: ProcessToDo[] = []
let actions: EventButton[] = []
const buttonsQuery = createQuery()
$: buttonsQuery.query(
process.class.EventButton,
{
card: card._id
},
(res) => {
actions = res
}
)
const executionQuery = createQuery()
$: executionQuery.query(
@@ -60,8 +72,41 @@
doneOn: new Date().getTime()
})
}
async function performAction (action: EventButton) {
await client.createDoc(process.class.ProcessCustomEvent, action.space, {
execution: action.execution,
eventType: action.eventType,
card: card._id
})
}
async function performRollback (execution: Execution) {
await client.createDoc(process.class.ProcessCustomEvent, execution.space, {
execution: execution._id,
eventType: 'rollback',
card: card._id
})
}
function getExecutionLabel (execution: Execution): string {
const pr = client.getModel().findObject(execution.process)
if (pr !== undefined) {
return `${pr.name}: `
}
return ''
}
$: rollbacks = docs.filter((d) => d.rollback.length > 0)
</script>
{#each todos as todo (todo._id)}
<Button kind={'primary'} label={getEmbeddedLabel(todo.title)} on:click={() => checkTodo(todo)} />
{/each}
{#each actions as action (action._id)}
<Button kind={'primary'} label={getEmbeddedLabel(action.title)} on:click={() => performAction(action)} />
{/each}
{#each rollbacks as rollback}
{getExecutionLabel(rollback)}
<Button kind={'dangerous'} label={process.string.Rollback} on:click={() => performRollback(rollback)} />
{/each}
+4 -2
View File
@@ -85,7 +85,8 @@ import {
matchCardCheck,
subProcessesDoneCheck,
fieldChangesCheck,
subProcessMatchCheck
subProcessMatchCheck,
eventCheck
} from './utils'
import FieldChangesEditor from './components/settings/FieldChangesEditor.svelte'
@@ -173,7 +174,8 @@ export default async (): Promise<Resources> => ({
SubProcessesDoneCheck: subProcessesDoneCheck,
SubProcessMatchCheck: subProcessMatchCheck,
ToDo: todoTranstionCheck,
Time: timeTransitionCheck
Time: timeTransitionCheck,
OnEventCheck: eventCheck
},
function: {
ShowDoneQuery: showDoneQuery,
+3
View File
@@ -68,6 +68,8 @@ export default mergeIds(processId, process, {
CardUpdateEditor: '' as AnyComponent,
CardUpdatePresenter: '' as AnyComponent,
FieldChangesEditor: '' as AnyComponent,
OnEventEditor: '' as AnyComponent,
OnEventPresenter: '' as AnyComponent,
ToDoSettingPresenter: '' as AnyComponent,
TimeEditor: '' as AnyComponent,
TimePresenter: '' as AnyComponent,
@@ -174,6 +176,7 @@ export default mergeIds(processId, process, {
WaitUntil: '' as IntlString,
WhenCardMatches: '' as IntlString,
WhenFieldChanges: '' as IntlString,
OnEvent: '' as IntlString,
Result: '' as IntlString,
RequestResult: '' as IntlString,
NoResultRequired: '' as IntlString,
+10
View File
@@ -617,6 +617,16 @@ export function timeTransitionCheck (
return params.value <= Date.now()
}
export function eventCheck (
client: Client,
execution: Execution,
params: Record<string, any>,
context: Record<string, any>
): boolean {
if (params.eventType === undefined) return false
return context.eventType === params.eventType
}
export function matchCardCheck (
client: Client,
execution: Execution,
+25 -4
View File
@@ -158,6 +158,19 @@ export interface UserResult {
type: Type<any>
}
export interface ProcessCustomEvent extends Doc {
eventType: string
execution: Ref<Execution>
card: Ref<Card>
}
export interface EventButton extends Doc {
title: string
eventType: string
execution: Ref<Execution>
card: Ref<Card>
}
export interface Method<T extends Doc> extends Doc {
label: IntlString
objectClass: Ref<Class<T>>
@@ -206,12 +219,17 @@ export default plugin(processId, {
ProcessFunction: '' as Ref<Class<ProcessFunction>>,
Transition: '' as Ref<Class<Transition>>,
Trigger: '' as Ref<Class<Trigger>>,
ProcessCustomEvent: '' as Ref<Class<ProcessCustomEvent>>,
ExecutionLog: '' as Ref<Class<ExecutionLog>>,
UpdateCriteriaComponent: '' as Ref<Class<UpdateCriteriaComponent>>
UpdateCriteriaComponent: '' as Ref<Class<UpdateCriteriaComponent>>,
EventButton: '' as Ref<Class<EventButton>>
},
method: {
RunSubProcess: '' as Ref<Method<Process>>,
CreateAction: '' as Ref<Method<EventButton>>,
CancellAction: '' as Ref<Method<EventButton>>,
CreateToDo: '' as Ref<Method<ProcessToDo>>,
CloseToDo: '' as Ref<Method<ProcessToDo>>,
UpdateCard: '' as Ref<Method<Card>>,
CreateCard: '' as Ref<Method<Card>>,
AddRelation: '' as Ref<Method<Association>>,
@@ -226,7 +244,8 @@ export default plugin(processId, {
OnToDoRemove: '' as Ref<Trigger>,
OnExecutionStart: '' as Ref<Trigger>,
OnExecutionContinue: '' as Ref<Trigger>,
OnTime: '' as Ref<Trigger>
OnTime: '' as Ref<Trigger>,
OnEvent: '' as Ref<Trigger>
},
triggerCheck: {
ToDo: '' as Resource<CheckFunc>,
@@ -234,7 +253,8 @@ export default plugin(processId, {
FieldChangedCheck: '' as Resource<CheckFunc>,
SubProcessesDoneCheck: '' as Resource<CheckFunc>,
SubProcessMatchCheck: '' as Resource<CheckFunc>,
Time: '' as Resource<CheckFunc>
Time: '' as Resource<CheckFunc>,
OnEventCheck: '' as Resource<CheckFunc>
},
string: {
Method: '' as IntlString,
@@ -276,7 +296,8 @@ export default plugin(processId, {
WaitSubprocesses: '' as Asset,
ToDoRemove: '' as Asset,
Start: '' as Asset,
Time: '' as Asset
Time: '' as Asset,
OnEvent: '' as Asset
},
function: {
FirstMatchValue: '' as Ref<ProcessFunction>,
@@ -145,6 +145,16 @@ export function MatchCardCheck (
return res.length > 0
}
export function EventCheck (
control: ProcessControl,
execution: Execution,
params: Record<string, any>,
context: Record<string, any>
): boolean {
if (params.eventType === undefined) return false
return context.eventType === params.eventType
}
export function FieldChangedCheck (
control: ProcessControl,
execution: Execution,
@@ -299,23 +309,23 @@ export async function AddTag (
): Promise<ExecuteResult> {
const { _id, props } = params
if (_id === undefined) throw processError(process.error.RequiredParamsNotProvided, { params: '_id' })
const tagId = _id as Ref<Tag>
const res: Tx[] = []
const _process = control.client.getModel().findObject(execution.process)
if (_process === undefined) throw processError(process.error.ObjectNotFound, { _id: execution.process })
const tx = control.client.txFactory.createTxMixin(
execution.card,
_process.masterTag,
core.space.Workspace,
_id as Ref<Tag>,
props
)
const tx = control.client.txFactory.createTxMixin(execution.card, _process.masterTag, execution.space, tagId, props)
res.push(tx)
const card = control.cache.get(execution.card)
const cardWithMixin =
card !== undefined ? TxProcessor.updateMixin4Doc(control.client.getHierarchy().clone(card), tx) : undefined
const rollback = control.client.txFactory.createTxUpdateDoc(_process.masterTag, execution.space, execution.card, {
$unset: { [tagId]: true }
})
return {
txes: res,
rollback: undefined,
rollback: [rollback],
context: [
{
_id: execution.card,
@@ -337,6 +347,7 @@ export async function RunSubProcess (
if (target === undefined) throw processError(process.error.ObjectNotFound, { _id: processId })
const res: Tx[] = []
const resultContext: SuccessExecutionContext[] = []
const rollback: Tx[] = []
for (const _card of Array.isArray(card) ? card : [card]) {
if (target.parallelExecutionForbidden === true) {
const currentExecution = await control.client.findAll(process.class.Execution, {
@@ -370,13 +381,15 @@ export async function RunSubProcess (
_id
)
rollback.push(control.client.txFactory.createTxRemoveDoc(process.class.Execution, core.space.Workspace, _id))
res.push(tx)
resultContext.push({
_id,
value: TxProcessor.createDoc2Doc(tx, true)
})
}
return { txes: res, rollback: undefined, context: resultContext }
return { txes: res, rollback, context: resultContext }
}
export async function CreateToDo (
+51 -4
View File
@@ -38,7 +38,8 @@ import process, {
State,
Step,
Transition,
isUpdateTx
isUpdateTx,
ProcessCustomEvent
} from '@hcengineering/process'
import { QueueTopic, TriggerControl } from '@hcengineering/server-core'
import { ProcessMessage } from '@hcengineering/server-process'
@@ -95,7 +96,8 @@ import {
CheckSubProcessesDone,
CheckSubProcessMatch,
CheckTime,
FieldChangedCheck
FieldChangedCheck,
EventCheck
} from './functions'
import { ToDoCancellRollback, ToDoCloseRollback } from './rollback'
@@ -141,6 +143,31 @@ export async function OnProcessToDoClose (txes: Tx[], control: TriggerControl):
return res
}
export async function OnCustomEvent (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
for (const tx of txes) {
if (tx._class !== core.class.TxCreateDoc) continue
const createTx = tx as TxCreateDoc<ProcessCustomEvent>
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<Tx[]> {
for (const tx of txes) {
if (tx._class !== core.class.TxCreateDoc) continue
@@ -249,6 +276,23 @@ export async function OnStateRemove (txes: Tx[], control: TriggerControl): Promi
return res
}
export async function OnExecutionRemove (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = []
for (const tx of txes) {
if (tx._class !== core.class.TxRemoveDoc) continue
const cudTx = tx as TxRemoveDoc<Execution>
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
}
export async function OnTransition (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = []
for (const tx of txes) {
@@ -400,7 +444,8 @@ export default async () => ({
MatchCardCheck,
CheckSubProcessesDone,
CheckSubProcessMatch,
CheckTime
CheckTime,
EventCheck
},
transform: {
CurrentDate,
@@ -454,6 +499,8 @@ export default async () => ({
OnExecutionCreate,
OnProcessToDoClose,
OnProcessToDoRemove,
OnExecutionContinue
OnExecutionContinue,
OnCustomEvent,
OnExecutionRemove
}
})
+5 -2
View File
@@ -60,7 +60,8 @@ export default plugin(serverProcessId, {
FieldChangedCheck: '' as Resource<CheckFunc>,
CheckSubProcessesDone: '' as Resource<CheckFunc>,
CheckSubProcessMatch: '' as Resource<CheckFunc>,
CheckTime: '' as Resource<CheckFunc>
CheckTime: '' as Resource<CheckFunc>,
EventCheck: '' as Resource<CheckFunc>
},
transform: {
FirstValue: '' as Resource<TransformFunc>,
@@ -110,6 +111,8 @@ export default plugin(serverProcessId, {
OnExecutionCreate: '' as Resource<TriggerFunc>,
OnProcessToDoClose: '' as Resource<TriggerFunc>,
OnProcessToDoRemove: '' as Resource<TriggerFunc>,
OnExecutionContinue: '' as Resource<TriggerFunc>
OnExecutionContinue: '' as Resource<TriggerFunc>,
OnCustomEvent: '' as Resource<TriggerFunc>,
OnExecutionRemove: '' as Resource<TriggerFunc>
}
})
+39 -23
View File
@@ -83,7 +83,7 @@ export async function messageHandler (record: ProcessMessage, ws: WorkspaceUuid,
ctx.info('Processing event', { event: record.event, ws, record })
if (record.execution !== undefined) {
const execution = await control.client.findOne(process.class.Execution, { _id: record.execution })
if (execution !== undefined && isActiveExecution(execution, record.event)) {
if (execution !== undefined) {
await processExecution(control, record, execution)
}
} else if (record.card !== undefined) {
@@ -140,9 +140,7 @@ async function processCardExecutions (control: ProcessControl, record: ProcessMe
currentState: { $in: Array.from(states) }
})
for (const execution of executions) {
if (isActiveExecution(execution, record.event)) {
await processExecution(control, record, execution)
}
await processExecution(control, record, execution)
}
}
@@ -263,26 +261,36 @@ async function executeResultSet (
}
async function processExecution (control: ProcessControl, record: ProcessMessage, execution: Execution): Promise<void> {
await checkToDoResult(control, record, execution)
const transition = await findTransitions(control, record, execution)
if (transition !== undefined) {
await execute(execution, transition, control)
} else {
if (record.event === process.trigger.OnToDoRemove) {
const rollbackResult = await checkRollback(control, record, execution)
if (rollbackResult) return
if (isActiveExecution(execution, record.event)) {
await checkToDoResult(control, record, execution)
const transition = await findTransitions(control, record, execution)
if (transition !== undefined) {
await execute(execution, transition, control)
return
} else {
control.ctx.info('No transition found for event', {
event: record.event,
execution: execution._id,
state: execution.currentState
})
}
control.ctx.info('No transition found for event', {
event: record.event,
execution: execution._id,
state: execution.currentState
})
}
if (isRollback(record)) {
await rollback(control, record, execution)
}
}
async function checkRollback (control: ProcessControl, record: ProcessMessage, execution: Execution): Promise<boolean> {
const todo = record.context.todo as ProcessToDo
if (!todo?.withRollback) return false
function isRollback (record: ProcessMessage): boolean {
if (record.event === process.trigger.OnEvent && record.context.eventType === 'rollback') {
return true
}
if (record.event === process.trigger.OnToDoRemove && record.context.todo?.withRollback === true) {
return true
}
return false
}
async function rollback (control: ProcessControl, record: ProcessMessage, execution: Execution): Promise<void> {
const rollbackTxes = execution.rollback.pop() ?? []
for (const tx of rollbackTxes) {
const timeout = setTimeout(() => {
@@ -294,7 +302,6 @@ async function checkRollback (control: ProcessControl, record: ProcessMessage, e
await control.client.update(execution, {
rollback: execution.rollback
})
return true
}
async function getTriggerRollback (triggger: TriggerImpl, control: ProcessControl): Promise<Tx | undefined> {
@@ -334,6 +341,7 @@ async function executeTransition (
_transition: Transition,
control: ProcessControl
): Promise<void> {
let nested = false
let transition: Transition | undefined = _transition
while (transition !== undefined) {
let deep = control.cache.get(execution._id + 'transition') ?? 0
@@ -391,7 +399,7 @@ async function executeTransition (
if (isError(actionResult)) {
errors.push(actionResult)
} else {
if (actionResult.rollback !== undefined) {
if (actionResult.rollback !== undefined && actionResult.rollback.length > 0) {
rollback.push(...actionResult.rollback)
}
res.push(...actionResult.txes)
@@ -419,7 +427,14 @@ async function executeTransition (
}
}
if (!disableRollback) {
execution.rollback.push(rollback)
if (nested) {
const last = execution.rollback.pop() ?? []
execution.rollback.push([...last, ...rollback])
} else {
execution.rollback.push(rollback)
}
} else {
execution.rollback = []
}
const executionUpdate = getDiffUpdate(execution, {
currentState: state._id,
@@ -452,6 +467,7 @@ async function executeTransition (
}
TxProcessor.applyUpdate(execution, executionUpdate)
transition = await checkNext(control, execution, context)
nested = true
if (transition === undefined) {
await setNextTimers(control, execution)
}