Extract processor (#9614)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov
2025-07-30 18:06:22 +07:00
committed by GitHub
parent f3c4de0a44
commit 5dd225dd51
36 changed files with 1571 additions and 1436 deletions
@@ -1,21 +0,0 @@
//
// 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 { ExecutionError } from '@hcengineering/process'
import { ExecuteResult } from '@hcengineering/server-process'
export function isError (value: ExecuteResult | any): value is ExecutionError {
return (value as ExecutionError)?.error !== undefined
}
@@ -0,0 +1,212 @@
//
// 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, MasterTag } from '@hcengineering/card'
import core, { Association, Data, Doc, generateId, Hierarchy, matchQuery, Ref, Relation, Tx } from '@hcengineering/core'
import process, {
Execution,
ExecutionContext,
ExecutionStatus,
MethodParams,
Process,
processError,
ProcessToDo
} from '@hcengineering/process'
import { ExecuteResult, ProcessControl } from '@hcengineering/server-process'
import time, { ToDoPriority } from '@hcengineering/time'
export function CheckToDo (params: Record<string, any>, context: Record<string, any>): boolean {
if (params._id === undefined) return false
if (context.todo === undefined) return false
return context.todo._id === params._id
}
export function OnCardUpdateCheck (
params: Record<string, any>,
context: Record<string, any>,
hierarchy: Hierarchy
): boolean {
if (context.card === undefined) return false
const res = matchQuery([context.card], params, context.card._class, hierarchy, true)
return res.length > 0
}
export async function AddRelation (
params: MethodParams<Relation>,
execution: Execution,
control: ProcessControl
): Promise<ExecuteResult | undefined> {
const _id = generateId<Relation>()
const association = params.association as Ref<Association>
if (isEmpty(association)) {
throw processError(process.error.RequiredParamsNotProvided, { params: 'association' })
}
if (isEmpty(params._id)) {
throw processError(process.error.RequiredParamsNotProvided, { params: '_id' })
}
if (isEmpty(params.direction)) {
throw processError(process.error.RequiredParamsNotProvided, { params: 'direction' })
}
const targetId = params._id as Ref<Doc>
const direction = params.direction as 'A' | 'B'
const docA = direction === 'A' ? targetId : execution.card
const docB = direction === 'A' ? execution.card : targetId
const data: Data<Relation> = {
association,
docA,
docB
}
const res: Tx[] = [control.client.txFactory.createTxCreateDoc(core.class.Relation, core.space.Workspace, data, _id)]
const rollback: Tx[] = [control.client.txFactory.createTxRemoveDoc(core.class.Relation, core.space.Workspace, _id)]
return { txes: res, rollback, context: _id }
}
export async function UpdateCard (
params: MethodParams<Card>,
execution: Execution,
control: ProcessControl
): Promise<ExecuteResult | undefined> {
if (Object.keys(params).length === 0) return
const target = control.cache.get(execution.card)
if (target === undefined) return
const update: Record<string, any> = {}
const prevValue: Record<string, any> = {}
for (const key in params) {
prevValue[key] = target[key]
update[key] = (params as any)[key]
}
const res: Tx[] = [control.client.txFactory.createTxUpdateDoc(target._class, target.space, target._id, update)]
const rollback: Tx[] = [
control.client.txFactory.createTxUpdateDoc(target._class, target.space, target._id, prevValue)
]
return { txes: res, rollback, context: null }
}
export async function RunSubProcess (
params: MethodParams<Execution>,
execution: Execution,
control: ProcessControl
): Promise<ExecuteResult | undefined> {
if (params._id === undefined) return
const card = params.card ?? execution.card
const processId = params._id as Ref<Process>
const target = control.client.getModel().findObject(processId)
if (target === undefined) return
const res: Tx[] = []
const context: Ref<Execution>[] = []
for (const _card of Array.isArray(card) ? card : [card]) {
if (target.parallelExecutionForbidden === true) {
const currentExecution = await control.client.findAll(process.class.Execution, {
process: target._id,
card: _card,
done: false
})
if (currentExecution.length > 0) {
// todo, show erro after merge another pr
continue
}
}
const initTransition = control.client
.getModel()
.findAllSync(process.class.Transition, { process: target._id, from: null })[0]
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const emptyContext = {} as ExecutionContext
const id = generateId<Execution>()
res.push(
control.client.txFactory.createTxCreateDoc(
process.class.Execution,
core.space.Workspace,
{
process: processId,
currentState: initTransition.to,
card: _card,
context: emptyContext,
status: ExecutionStatus.Active,
rollback: [],
parentId: execution._id
},
id
)
)
context.push(id)
}
return { txes: res, rollback: undefined, context }
}
export async function CreateToDo (
params: MethodParams<ProcessToDo>,
execution: Execution,
control: ProcessControl
): Promise<ExecuteResult | undefined> {
for (const key in { user: params.user, title: params.title }) {
const val = (params as any)[key]
if (isEmpty(val)) {
throw processError(process.error.RequiredParamsNotProvided, { params: key })
}
}
if (params.user === undefined || params.title === undefined) return
const res: Tx[] = []
const rollback: Tx[] = []
const id = generateId<ProcessToDo>()
res.push(
control.client.txFactory.createTxCreateDoc(
process.class.ProcessToDo,
time.space.ToDos,
{
attachedTo: execution.card,
attachedToClass: cardPlugin.class.Card,
collection: 'todos',
workslots: 0,
execution: execution._id,
title: params.title,
user: params.user,
description: params.description ?? '',
dueDate: params.dueDate,
priority: params.priority ?? ToDoPriority.NoPriority,
visibility: 'public',
rank: '',
withRollback: params.withRollback ?? false
},
id
)
)
return { txes: res, rollback, context: id }
}
export async function CreateCard (
params: MethodParams<Card>,
execution: Execution,
control: ProcessControl
): Promise<ExecuteResult | undefined> {
const { _class, title, ...attrs } = params
for (const key in { _class, title }) {
const val = (params as any)[key]
if (isEmpty(val)) {
throw processError(process.error.RequiredParamsNotProvided, { params: key })
}
}
const _id = generateId<Card>()
const data = {
title,
...attrs
} as any
const res: Tx[] = [control.client.txFactory.createTxCreateDoc(_class as Ref<MasterTag>, execution.space, data, _id)]
const rollback: Tx[] = [control.client.txFactory.createTxRemoveDoc(_class as Ref<MasterTag>, execution.space, _id)]
return { txes: res, rollback, context: _id }
}
function isEmpty (value: any): boolean {
return value === undefined || value === null || (typeof value === 'string' && value.trim() === '')
}
+83 -768
View File
@@ -13,57 +13,20 @@
// limitations under the License.
//
import cardPlugin, { Card, MasterTag } from '@hcengineering/card'
import contact, { Employee } from '@hcengineering/contact'
import core, {
ArrOf,
Association,
Data,
Doc,
generateId,
getObjectValue,
Hierarchy,
matchQuery,
Ref,
RefTo,
Relation,
Tx,
TxCreateDoc,
TxCUD,
TxProcessor,
TxRemoveDoc,
TxUpdateDoc
} from '@hcengineering/core'
import { getEmbeddedLabel, getResource } from '@hcengineering/platform'
import cardPlugin, { Card } from '@hcengineering/card'
import core, { Tx, TxCreateDoc, TxCUD, TxProcessor, TxRemoveDoc, TxUpdateDoc } from '@hcengineering/core'
import process, {
ContextId,
Execution,
ExecutionContext,
ExecutionError,
ExecutionLogAction,
ExecutionStatus,
MethodParams,
parseContext,
parseError,
Process,
ProcessContext,
processError,
ProcessError,
ProcessToDo,
SelectedContext,
SelectedContextFunc,
SelectedExecutonContext,
SelectedNested,
SelectedRelation,
SelectedUserRequest,
State,
Step,
Transition
} from '@hcengineering/process'
import { TriggerControl } from '@hcengineering/server-core'
import serverProcess, { ExecuteResult, MethodImpl } from '@hcengineering/server-process'
import time, { ToDoPriority } from '@hcengineering/time'
import { isError } from './errors'
import { QueueTopic, TriggerControl } from '@hcengineering/server-core'
import { ProcessMessage } from '@hcengineering/server-process'
import {
Absolute,
Add,
@@ -85,17 +48,43 @@ import {
Prepend,
Random,
Remove,
RemoveFirst,
RemoveLast,
Replace,
ReplaceAll,
RoleContext,
Round,
Split,
Subtract,
RemoveFirst,
RemoveLast,
Trim,
UpperCase
} from './transform'
import { getAttributeValue, pickTransition } from './utils'
import {
RunSubProcess,
CreateToDo,
UpdateCard,
CreateCard,
AddRelation,
CheckToDo,
OnCardUpdateCheck
} from './functions'
import { ToDoCancellRollback, ToDoCloseRollback } from './rollback'
async function putEventToQueue (value: Omit<ProcessMessage, 'account'>, control: TriggerControl): Promise<void> {
if (control.queue === undefined) return
const producer = control.queue.getProducer<ProcessMessage>(control.ctx.newChild('queue', {}), QueueTopic.Process)
try {
await producer.send(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<Tx[]> {
const res: Tx[] = []
@@ -108,482 +97,16 @@ export async function OnProcessToDoClose (txes: Tx[], control: TriggerControl):
await control.findAll(control.ctx, process.class.ProcessToDo, { _id: updateTx.objectId }, { limit: 1 })
)[0]
if (todo === undefined) continue
const execution = (
await control.findAll(control.ctx, process.class.Execution, { _id: todo.execution }, { limit: 1 })
)[0]
if (execution === undefined) continue
const _process = await control.modelDb.findOne(process.class.Process, { _id: execution.process })
if (_process === undefined) continue
const transitions = control.modelDb.findAllSync(process.class.Transition, {
from: execution.currentState,
process: _process._id,
trigger: process.trigger.OnToDoClose
})
const transition = await pickTransition(control, execution, transitions, todo)
if (transition === undefined) continue
const rollback: Tx[] = [control.txFactory.createTxUpdateDoc(todo._class, todo.space, todo._id, { doneOn: null })]
res.push(...(await executeTransition(execution, transition, control, rollback)))
}
return res
}
async function executeTransition (
execution: Execution,
transition: Transition,
control: TriggerControl,
rollback: Tx[],
disableRollback = false
): Promise<Tx[]> {
let deep = control.contextCache.get(execution._id + 'transition') ?? 1
deep++
control.contextCache.set(execution._id + 'transition', deep)
if (deep > 100) {
const error = parseError(
processError(process.error.TooDeepTransitionRecursion, undefined, undefined, true),
transition._id
)
return [control.txFactory.createTxUpdateDoc(execution._class, execution.space, execution._id, { error: [error] })]
}
const res: Tx[] = []
const _process = control.modelDb.findObject(execution.process)
if (_process === undefined) return res
const state = control.modelDb.findObject(transition.to)
if (state === undefined) return res
const isDone =
control.modelDb.findAllSync(process.class.Transition, {
from: transition.to,
process: transition.process
}).length === 0
const errors: ExecutionError[] = []
if (execution.currentState !== null) {
rollback.push(
control.txFactory.createTxUpdateDoc(execution._class, execution.space, execution._id, {
currentState: execution.currentState,
status: execution.status
})
)
} else {
rollback.push(control.txFactory.createTxRemoveDoc(execution._class, execution.space, execution._id))
}
for (const action of transition.actions) {
const actionResult = await executeAction(action, transition._id, execution, control)
if (isError(actionResult)) {
errors.push(actionResult)
} else {
if (actionResult.rollback !== undefined) {
rollback.push(...actionResult.rollback)
}
res.push(...actionResult.txes)
}
}
if (!disableRollback) {
execution.rollback.push(rollback)
}
if (isDone && execution.parentId !== undefined) {
const parentWaitTxes = await checkParent(execution, control)
if (parentWaitTxes !== undefined) {
res.push(...parentWaitTxes)
}
}
res.push(
control.txFactory.createTxUpdateDoc(execution._class, execution.space, execution._id, {
rollback: execution.rollback,
context: execution.context,
currentState: state._id,
status: isDone ? ExecutionStatus.Done : ExecutionStatus.Active
})
)
res.push(
control.txFactory.createTxCreateDoc(process.class.ExecutionLog, execution.space, {
execution: execution._id,
process: execution.process,
card: execution.card,
transition: transition._id,
action: ExecutionLogAction.Transition
})
)
if (errors.length === 0) {
return res
} else {
return [control.txFactory.createTxUpdateDoc(execution._class, execution.space, execution._id, { error: errors })]
}
}
async function executeAction<T extends Doc> (
action: Step<T>,
transition: Ref<Transition>,
execution: Execution,
control: TriggerControl
): Promise<ExecuteResult> {
try {
const method = control.modelDb.findObject(action.methodId)
if (method === undefined) throw processError(process.error.MethodNotFound, { methodId: action.methodId }, {}, true)
const impl = control.hierarchy.as(method, serverProcess.mixin.MethodImpl) as MethodImpl<T>
if (impl === undefined) throw processError(process.error.MethodNotFound, { methodId: action.methodId }, {}, true)
const params = await fillParams(action.params, execution, control)
const f = await getResource(impl.func)
const res = await f(params, execution, control)
if (!isError(res) && action.context?._id != null && res.context != null) {
execution.context[action.context._id] = res.context
}
return res
} catch (err) {
if (err instanceof ProcessError) {
if (err.shouldLog) {
control.ctx.error(err.message, { props: err.props })
}
return parseError(err, transition)
} else {
const errorId = generateId()
control.ctx.error(err instanceof Error ? err.message : String(err), { errorId })
return parseError(processError(process.error.InternalServerError, { errorId }), transition)
}
}
}
async function fillValue (
value: any,
context: SelectedContext,
control: TriggerControl,
execution: Execution
): Promise<any> {
for (const func of context.functions ?? []) {
const transform = control.modelDb.findObject(func.func)
if (transform === undefined) throw processError(process.error.MethodNotFound, { methodId: func.func }, {}, true)
if (!control.hierarchy.hasMixin(transform, serverProcess.mixin.FuncImpl)) {
throw processError(process.error.MethodNotFound, { methodId: func.func }, {}, true)
}
const funcImpl = control.hierarchy.as(transform, serverProcess.mixin.FuncImpl)
const f = await getResource(funcImpl.func)
value = await f(value, func.props, control, execution)
}
return value
}
async function getNestedValue (
control: TriggerControl,
execution: Execution,
context: SelectedNested
): Promise<any | ExecutionError> {
const cardValue = await control.findAll(control.ctx, cardPlugin.class.Card, { _id: execution.card }, { limit: 1 })
if (cardValue.length === 0) throw processError(process.error.ObjectNotFound, { _id: execution.card }, {}, true)
const attr = control.hierarchy.findAttribute(cardValue[0]._class, context.path)
if (attr === undefined) throw processError(process.error.AttributeNotExists, { key: context.path })
const nestedValue = getObjectValue(context.path, cardValue[0])
if (nestedValue === undefined) throw processError(process.error.EmptyAttributeContextValue, {}, { attr: attr.label })
const parentType = attr.type._class === core.class.ArrOf ? (attr.type as ArrOf<Doc>).of : attr.type
const targetClass = parentType._class === core.class.RefTo ? (parentType as RefTo<Doc>).to : parentType._class
const target = await control.findAll(control.ctx, targetClass, {
_id: { $in: Array.isArray(nestedValue) ? nestedValue : [nestedValue] }
})
if (target.length === 0) throw processError(process.error.RelatedObjectNotFound, {}, { attr: attr.label })
const nested = control.hierarchy.findAttribute(targetClass, context.key)
if (context.sourceFunction !== undefined) {
const transform = control.modelDb.findObject(context.sourceFunction)
if (transform === undefined) {
throw processError(process.error.MethodNotFound, { methodId: context.sourceFunction }, {}, true)
}
if (!control.hierarchy.hasMixin(transform, serverProcess.mixin.FuncImpl)) {
throw processError(process.error.MethodNotFound, { methodId: context.sourceFunction }, {}, true)
}
const funcImpl = control.hierarchy.as(transform, serverProcess.mixin.FuncImpl)
const f = await getResource(funcImpl.func)
const reduced = await f(target, {}, control, execution)
const val = getObjectValue(context.key, reduced)
if (val == null) {
throw processError(
process.error.EmptyRelatedObjectValue,
{},
{ parent: attr.label, attr: nested?.label ?? getEmbeddedLabel(context.key) }
)
}
return val
}
const val = getObjectValue(context.key, target[0])
if (val == null) {
throw processError(
process.error.EmptyRelatedObjectValue,
{},
{ parent: attr.label, attr: nested?.label ?? getEmbeddedLabel(context.key) }
)
}
return val
}
async function getRelationValue (
control: TriggerControl,
execution: Execution,
context: SelectedRelation
): Promise<any> {
const assoc = control.modelDb.findObject(context.association)
if (assoc === undefined) throw processError(process.error.RelationNotExists, {})
const targetClass = context.direction === 'A' ? assoc.classA : assoc.classB
const q = context.direction === 'A' ? { docB: execution.card } : { docA: execution.card }
const relations = await control.findAll(control.ctx, core.class.Relation, { association: assoc._id, ...q })
const name = context.direction === 'A' ? assoc.nameA : assoc.nameB
if (relations.length === 0) throw processError(process.error.RelatedObjectNotFound, { attr: name })
const ids = relations.map((it) => {
return context.direction === 'A' ? it.docA : it.docB
})
const target = await control.findAll(control.ctx, targetClass, { _id: { $in: ids } })
if (target.length === 0) throw processError(process.error.RelatedObjectNotFound, { attr: context.name })
const attr = context.key !== '' ? control.hierarchy.findAttribute(targetClass, context.key) : undefined
if (context.sourceFunction !== undefined) {
const transform = control.modelDb.findObject(context.sourceFunction)
if (transform === undefined) {
throw processError(process.error.MethodNotFound, { methodId: context.sourceFunction }, {}, true)
}
if (!control.hierarchy.hasMixin(transform, serverProcess.mixin.FuncImpl)) {
throw processError(process.error.MethodNotFound, { methodId: context.sourceFunction }, {}, true)
}
const funcImpl = control.hierarchy.as(transform, serverProcess.mixin.FuncImpl)
const f = await getResource(funcImpl.func)
const reduced = await f(target, {}, control, execution)
const val = Array.isArray(reduced)
? reduced.map((v) => getObjectValue(context.key, v))
: getObjectValue(context.key, reduced)
if (val == null) {
throw processError(
process.error.EmptyRelatedObjectValue,
{ parent: name },
{ attr: attr?.label ?? getEmbeddedLabel(context.name) }
)
}
return val
}
const val =
Array.isArray(target) && target.length > 1
? target.map((v) => getObjectValue(context.key, v))
: getObjectValue(context.key, target[0])
if (val == null) {
throw processError(
process.error.EmptyRelatedObjectValue,
{ parent: name },
{ attr: attr?.label ?? getEmbeddedLabel(context.name) }
)
}
return val
}
async function fillParams<T extends Doc> (
params: MethodParams<T>,
execution: Execution,
control: TriggerControl
): Promise<MethodParams<T>> {
const res: MethodParams<T> = {}
for (const key in params) {
const value = (params as any)[key]
const valueResult = await getContextValue(value, control, execution)
;(res as any)[key] = valueResult
}
return res
}
async function getContextValue (value: any, control: TriggerControl, execution: Execution): Promise<any> {
const context = parseContext(value)
if (context !== undefined) {
let value: any | undefined
try {
if (context.type === 'attribute') {
value = await getAttributeValue(control, execution, context)
} else if (context.type === 'relation') {
value = await getRelationValue(control, execution, context)
} else if (context.type === 'nested') {
value = await getNestedValue(control, execution, context)
} else if (context.type === 'userRequest') {
value = getUserRequestValue(control, execution, context)
} else if (context.type === 'function') {
value = await getFunctionValue(control, execution, context)
} else if (context.type === 'context') {
value = getExecutionContextValue(control, execution, context)
}
return await fillValue(value, context, control, execution)
} catch (err: any) {
if (err instanceof ProcessError && context.fallbackValue !== undefined) {
return await fillValue(context.fallbackValue, context, control, execution)
}
throw err
}
} else {
return value
}
}
async function getFunctionValue (
control: TriggerControl,
execution: Execution,
context: SelectedContextFunc
): Promise<any> {
const func = control.modelDb.findObject(context.func)
if (func === undefined) throw processError(process.error.MethodNotFound, { methodId: context.func }, {}, true)
const impl = control.hierarchy.as(func, serverProcess.mixin.FuncImpl)
if (impl === undefined) throw processError(process.error.MethodNotFound, { methodId: context.func }, {}, true)
const f = await getResource(impl.func)
const res = await f(null, context.props, control, execution)
if (context.sourceFunction !== undefined) {
const transform = control.modelDb.findObject(context.sourceFunction)
if (transform === undefined) {
throw processError(process.error.MethodNotFound, { methodId: context.sourceFunction }, {}, true)
}
if (!control.hierarchy.hasMixin(transform, serverProcess.mixin.FuncImpl)) {
throw processError(process.error.MethodNotFound, { methodId: context.sourceFunction }, {}, true)
}
const funcImpl = control.hierarchy.as(transform, serverProcess.mixin.FuncImpl)
const f = await getResource(funcImpl.func)
const val = await f(res, {}, control, execution)
if (val == null) {
throw processError(process.error.EmptyFunctionResult, {}, { func: func.label })
}
return val
}
if (res == null) {
throw processError(process.error.EmptyFunctionResult, {}, { func: func.label })
}
return res
}
function getUserRequestValue (control: TriggerControl, execution: Execution, context: SelectedUserRequest): any {
const userContext = execution.context[context.id]
if (userContext !== undefined) return userContext
const attr = control.hierarchy.findAttribute(context._class, context.key)
throw processError(
process.error.UserRequestedValueNotProvided,
{},
{ attr: attr?.label ?? getEmbeddedLabel(context.key) }
)
}
function getExecutionContextValue (
control: TriggerControl,
execution: Execution,
context: SelectedExecutonContext
): any {
const userContext = execution.context[context.id]
if (userContext !== undefined) return userContext
const _process = control.modelDb.findObject(execution.process)
if (_process === undefined) return
const ctx = _process.context[context.id]
if (ctx === undefined) return
throw processError(process.error.ContextValueNotProvided, { name: ctx.name })
}
async function initState (execution: Execution, control: TriggerControl): Promise<Tx[]> {
const _process = control.modelDb.findObject(execution.process)
if (_process === undefined) return []
const transition = control.modelDb.findAllSync(process.class.Transition, {
process: execution.process,
from: null
})[0]
if (transition === undefined) return []
const errors: ExecutionError[] = []
const res: Tx[] = []
const rollback: Tx[] = []
rollback.push(control.txFactory.createTxRemoveDoc(execution._class, execution.space, execution._id))
for (const action of transition.actions) {
const actionResult = await executeAction(action, transition._id, execution, control)
if (isError(actionResult)) {
errors.push(actionResult)
} else {
if (actionResult.rollback !== undefined) {
rollback.push(...actionResult.rollback)
}
res.push(...actionResult.txes)
}
}
execution.rollback.push(rollback)
res.push(
control.txFactory.createTxUpdateDoc(execution._class, execution.space, execution._id, {
currentState: transition.to,
rollback: execution.rollback,
context: execution.context,
status: ExecutionStatus.Active
})
)
res.push(
control.txFactory.createTxCreateDoc(process.class.ExecutionLog, execution.space, {
execution: execution._id,
process: execution.process,
card: execution.card,
transition: transition._id,
action: ExecutionLogAction.Started
})
)
if (errors.length === 0) {
return res
} else {
return [control.txFactory.createTxUpdateDoc(execution._class, execution.space, execution._id, { error: errors })]
}
}
async function checkParent (execution: Execution, control: TriggerControl): Promise<Tx[] | undefined> {
const subProcesses = await control.findAll(control.ctx, process.class.Execution, {
parentId: execution.parentId,
status: ExecutionStatus.Active
})
const filtered = subProcesses.filter((it) => it._id !== execution._id)
if (filtered.length !== 0) return
const parent = (await control.findAll(control.ctx, process.class.Execution, { _id: execution.parentId }))[0]
if (parent === undefined) return
const res: Tx[] = []
const _process = control.modelDb.findObject(parent.process)
if (_process === undefined) return res
if (parent.status !== ExecutionStatus.Active) return res
const transitions = control.modelDb.findAllSync(process.class.Transition, {
from: parent.currentState,
process: parent.process,
trigger: process.trigger.OnSubProcessesDone
})
const transition = await pickTransition(control, execution, transitions, execution)
if (transition === undefined) return res
res.push(...(await executeTransition(parent, transition, control, [], true)))
return res
}
export async function OnExecutionTransition (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = []
for (const tx of txes) {
if (tx._class !== core.class.TxUpdateDoc) continue
const updateTx = tx as TxUpdateDoc<Execution>
if (!control.hierarchy.isDerived(updateTx.objectClass, process.class.Execution)) continue
if (updateTx.operations.currentState === undefined) continue
const execution = (
await control.findAll(control.ctx, process.class.Execution, { _id: updateTx.objectId }, { limit: 1 })
)[0]
const cardUpdateTransitions = control.modelDb.findAllSync(process.class.Transition, {
from: execution.currentState,
process: execution.process,
trigger: process.trigger.OnCardUpdate
})
if (cardUpdateTransitions.length > 0) {
const doc = (await control.findAll(control.ctx, cardPlugin.class.Card, { _id: execution.card }, { limit: 1 }))[0]
if (doc !== undefined) {
const transition = await pickTransition(control, execution, cardUpdateTransitions, doc)
if (transition !== undefined) {
res.push(...(await executeTransition(execution, transition, control, [])))
continue
await putEventToQueue(
{
event: process.trigger.OnToDoClose,
execution: todo.execution,
context: {
todo
}
}
} else {
const subProcessesTransitions = control.modelDb.findAllSync(process.class.Transition, {
from: execution.currentState,
process: execution.process,
trigger: process.trigger.OnSubProcessesDone
})
if (subProcessesTransitions.length > 0) {
const subProcesses = await control.findAll(control.ctx, process.class.Execution, {
parentId: execution._id,
status: ExecutionStatus.Active
})
if (subProcesses.length === 0) {
const transition = await pickTransition(control, execution, subProcessesTransitions, execution)
if (transition !== undefined) {
res.push(...(await executeTransition(execution, transition, control, [])))
continue
}
}
}
}
},
control
)
}
return res
}
@@ -595,8 +118,14 @@ export async function OnExecutionCreate (txes: Tx[], control: TriggerControl): P
const createTx = tx as TxCreateDoc<Execution>
if (!control.hierarchy.isDerived(createTx.objectClass, process.class.Execution)) continue
const execution = TxProcessor.createDoc2Doc(createTx)
res.push(...(await initState(execution, control)))
await putEventToQueue(
{
event: process.trigger.OnExecutionStart,
execution: execution._id,
context: {}
},
control
)
}
return res
}
@@ -609,221 +138,20 @@ export async function OnProcessToDoRemove (txes: Tx[], control: TriggerControl):
if (!control.hierarchy.isDerived(removeTx.objectClass, process.class.ProcessToDo)) continue
const removedTodo = control.removedMap.get(removeTx.objectId) as ProcessToDo
if (removedTodo === undefined) continue
const execution = (await control.findAll(control.ctx, process.class.Execution, { _id: removedTodo.execution }))[0]
if (execution === undefined) continue
if (removedTodo.withRollback) {
const rollback = execution.rollback.pop()
if (rollback !== undefined) {
for (const rollbackTx of rollback) {
res.push(rollbackTx)
await putEventToQueue(
{
event: process.trigger.OnToDoRemove,
execution: removedTodo.execution,
context: {
todo: removedTodo
}
res.push(
control.txFactory.createTxCreateDoc(process.class.ExecutionLog, execution.space, {
execution: execution._id,
process: execution.process,
card: execution.card,
action: ExecutionLogAction.Rollback
})
)
}
} else {
const transitions = control.modelDb.findAllSync(process.class.Transition, {
from: execution.currentState,
process: execution.process,
trigger: process.trigger.OnToDoRemove
})
const transition = await pickTransition(control, execution, transitions, removedTodo)
if (transition === undefined) continue
const rollback: Tx[] = [
control.txFactory.createTxCreateDoc(
removedTodo._class,
removedTodo.space,
{
...removedTodo
},
removedTodo._id,
removedTodo.modifiedOn,
removeTx.modifiedBy
)
]
res.push(...(await executeTransition(execution, transition, control, rollback)))
}
},
control
)
}
return res
}
export async function CreateToDo (
params: MethodParams<ProcessToDo>,
execution: Execution,
control: TriggerControl
): Promise<ExecuteResult | undefined> {
for (const key in { user: params.user, title: params.title }) {
const val = (params as any)[key]
if (isEmpty(val)) {
throw processError(process.error.RequiredParamsNotProvided, { params: key })
}
}
if (params.user === undefined || params.title === undefined) return
const res: Tx[] = []
const rollback: Tx[] = []
const id = generateId<ProcessToDo>()
res.push(
control.txFactory.createTxCreateDoc(
process.class.ProcessToDo,
time.space.ToDos,
{
attachedTo: execution.card,
attachedToClass: cardPlugin.class.Card,
collection: 'todos',
workslots: 0,
execution: execution._id,
title: params.title,
user: params.user,
description: params.description ?? '',
dueDate: params.dueDate,
priority: params.priority ?? ToDoPriority.NoPriority,
visibility: 'public',
rank: '',
withRollback: params.withRollback ?? false
},
id
)
)
return { txes: res, rollback, context: id }
}
export async function CreateCard (
params: MethodParams<Card>,
execution: Execution,
control: TriggerControl
): Promise<ExecuteResult | undefined> {
const { _class, title, ...attrs } = params
for (const key in { _class, title }) {
const val = (params as any)[key]
if (isEmpty(val)) {
throw processError(process.error.RequiredParamsNotProvided, { params: key })
}
}
const _id = generateId<Card>()
const data = {
title,
...attrs
} as any
const res: Tx[] = [control.txFactory.createTxCreateDoc(_class as Ref<MasterTag>, execution.space, data, _id)]
const rollback: Tx[] = [control.txFactory.createTxRemoveDoc(_class as Ref<MasterTag>, execution.space, _id)]
return { txes: res, rollback, context: _id }
}
export async function AddRelation (
params: MethodParams<Relation>,
execution: Execution,
control: TriggerControl
): Promise<ExecuteResult | undefined> {
const _id = generateId<Relation>()
const association = params.association as Ref<Association>
if (isEmpty(association)) {
throw processError(process.error.RequiredParamsNotProvided, { params: 'association' })
}
if (isEmpty(params._id)) {
throw processError(process.error.RequiredParamsNotProvided, { params: '_id' })
}
if (isEmpty(params.direction)) {
throw processError(process.error.RequiredParamsNotProvided, { params: 'direction' })
}
const targetId = params._id as Ref<Doc>
const direction = params.direction as 'A' | 'B'
const docA = direction === 'A' ? targetId : execution.card
const docB = direction === 'A' ? execution.card : targetId
const data: Data<Relation> = {
association,
docA,
docB
}
const res: Tx[] = [control.txFactory.createTxCreateDoc(core.class.Relation, core.space.Workspace, data, _id)]
const rollback: Tx[] = [control.txFactory.createTxRemoveDoc(core.class.Relation, core.space.Workspace, _id)]
return { txes: res, rollback, context: _id }
}
export async function UpdateCard (
params: MethodParams<Card>,
execution: Execution,
control: TriggerControl
): Promise<ExecuteResult | undefined> {
if (Object.keys(params).length === 0) return
const target = (await control.findAll(control.ctx, cardPlugin.class.Card, { _id: execution.card }, { limit: 1 }))[0]
if (target === undefined) return
const update: Record<string, any> = {}
const prevValue: Record<string, any> = {}
for (const key in params) {
prevValue[key] = (target as any)[key]
update[key] = (params as any)[key]
}
const res: Tx[] = [control.txFactory.createTxUpdateDoc(target._class, target.space, target._id, update)]
const rollback: Tx[] = [control.txFactory.createTxUpdateDoc(target._class, target.space, target._id, prevValue)]
return { txes: res, rollback, context: null }
}
export async function RunSubProcess (
params: MethodParams<Execution>,
execution: Execution,
control: TriggerControl
): Promise<ExecuteResult | undefined> {
if (params._id === undefined) return
const card = params.card ?? execution.card
const processId = params._id as Ref<Process>
const target = control.modelDb.findObject(processId)
if (target === undefined) return
const res: Tx[] = []
const context: Ref<Execution>[] = []
for (const _card of Array.isArray(card) ? card : [card]) {
if (target.parallelExecutionForbidden === true) {
const currentExecution = await control.findAll(control.ctx, process.class.Execution, {
process: target._id,
card: _card,
done: false
})
if (currentExecution.length > 0) {
// todo, show erro after merge another pr
continue
}
}
const initTransition = control.modelDb.findAllSync(process.class.Transition, { process: target._id, from: null })[0]
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const emptyContext = {} as ExecutionContext
const id = generateId<Execution>()
res.push(
control.txFactory.createTxCreateDoc(
process.class.Execution,
core.space.Workspace,
{
process: processId,
currentState: initTransition.to,
card: _card,
context: emptyContext,
status: ExecutionStatus.Active,
rollback: [],
parentId: execution._id
},
id
)
)
context.push(id)
}
return { txes: res, rollback: undefined, context }
}
export async function RoleContext (
value: null,
props: Record<string, any>,
control: TriggerControl,
execution: Execution
): Promise<Ref<Employee>[]> {
const targetRole = props.target
if (targetRole === undefined) return []
const users = await control.findAll(control.ctx, contact.class.UserRole, { role: targetRole })
return users.map((it) => it.user)
}
export async function OnExecutionContinue (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = []
for (const tx of txes) {
@@ -837,13 +165,16 @@ export async function OnExecutionContinue (txes: Tx[], control: TriggerControl):
if (execution === undefined) continue
const error = execution.error
if (error == null) continue
res.push(control.txFactory.createTxUpdateDoc(execution._class, execution.space, execution._id, { error: null }))
const _process = await control.modelDb.findOne(process.class.Process, { _id: execution.process })
if (_process === undefined) continue
const transition = control.modelDb.findObject(error[0].transition)
if (transition !== undefined) {
res.push(...(await executeTransition(execution, transition, control, [])))
}
const transition = execution.error?.[0].transition
if (transition === undefined) continue
await putEventToQueue(
{
event: process.trigger.OnExecutionContinue,
execution: execution._id,
context: {}
},
control
)
}
return res
}
@@ -888,16 +219,6 @@ export async function OnStateRemove (txes: Tx[], control: TriggerControl): Promi
return res
}
export function CheckToDo (params: Record<string, any>, doc: Doc): boolean {
if (params._id === undefined) return false
return doc._id === params._id
}
export function OnCardUpdateCheck (params: Record<string, any>, doc: Doc, hierarchy: Hierarchy): boolean {
const res = matchQuery([doc], params, doc._class, hierarchy, true)
return res.length > 0
}
export async function OnTransition (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = []
for (const tx of txes) {
@@ -923,23 +244,18 @@ export async function OnCardUpdate (txes: Tx[], control: TriggerControl): Promis
if (!control.hierarchy.isDerived(tx._class, core.class.TxCUD)) continue
const cudTx = tx as TxCUD<Card>
if (!control.hierarchy.isDerived(cudTx.objectClass, cardPlugin.class.Card)) continue
const executions = await control.findAll(control.ctx, process.class.Execution, {
card: cudTx.objectId,
status: ExecutionStatus.Active
})
if (executions.length === 0) continue
const card = await control.findAll(control.ctx, cardPlugin.class.Card, { _id: cudTx.objectId }, { limit: 1 })
if (card.length === 0) continue
for (const execution of executions) {
const transitions = control.modelDb.findAllSync(process.class.Transition, {
from: execution.currentState,
process: execution.process,
trigger: process.trigger.OnCardUpdate
})
const transition = await pickTransition(control, execution, transitions, card[0])
if (transition === undefined) continue
res.push(...(await executeTransition(execution, transition, control, [])))
}
await putEventToQueue(
{
event: process.trigger.OnCardUpdate,
card: cudTx.objectId,
context: {
card: card[0]
}
},
control
)
}
return res
}
@@ -1009,10 +325,6 @@ async function syncContext (control: TriggerControl, _process: Process): Promise
}
}
function isEmpty (value: any): boolean {
return value === undefined || value === null || (typeof value === 'string' && value.trim() === '')
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({
func: {
@@ -1056,13 +368,16 @@ export default async () => ({
RemoveFirst,
RemoveLast
},
rollbacks: {
ToDoCloseRollback,
ToDoCancellRollback
},
trigger: {
OnProcessRemove,
OnStateRemove,
OnTransition,
OnCardUpdate,
OnExecutionCreate,
OnExecutionTransition,
OnProcessToDoClose,
OnProcessToDoRemove,
OnExecutionContinue
@@ -0,0 +1,39 @@
//
// 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 { Tx } from '@hcengineering/core'
import { ProcessToDo } from '@hcengineering/process'
import { ProcessControl } from '@hcengineering/server-process'
export function ToDoCloseRollback (context: Record<string, any>, control: ProcessControl): Tx | undefined {
const todo = context.todo as ProcessToDo
if (todo === undefined) return
return control.client.txFactory.createTxUpdateDoc(todo._class, todo.space, todo._id, { doneOn: null })
}
export function ToDoCancellRollback (context: Record<string, any>, control: ProcessControl): Tx | undefined {
const todo = context.todo as ProcessToDo
if (todo === undefined) return
return control.client.txFactory.createTxCreateDoc(
todo._class,
todo.space,
{
...todo
},
todo._id,
todo.modifiedOn,
todo.modifiedBy
)
}
@@ -13,9 +13,10 @@
// limitations under the License.
//
import { Doc, Timestamp } from '@hcengineering/core'
import contact, { Employee } from '@hcengineering/contact'
import { Doc, Ref, Timestamp } from '@hcengineering/core'
import { Execution, parseContext } from '@hcengineering/process'
import { TriggerControl } from '@hcengineering/server-core'
import { ProcessControl } from '@hcengineering/server-process'
import { getAttributeValue } from './utils'
// #region ArrayReduce
@@ -46,7 +47,7 @@ export function All (value: Doc[]): Doc[] {
export async function Insert (
value: Doc[],
props: Record<string, any>,
control: TriggerControl,
control: ProcessControl,
execution: Execution
): Promise<Doc[]> {
if (!Array.isArray(value)) return value
@@ -54,7 +55,7 @@ export async function Insert (
const context = parseContext(props.value)
if (context !== undefined) {
if (context.type === 'attribute') {
const addition = await getAttributeValue(control, execution, context)
const addition = getAttributeValue(control, execution, context)
value.push(addition)
}
} else {
@@ -66,7 +67,7 @@ export async function Insert (
export async function Remove (
value: Doc[],
props: Record<string, any>,
control: TriggerControl,
control: ProcessControl,
execution: Execution
): Promise<Doc[]> {
if (!Array.isArray(value)) return value
@@ -74,7 +75,7 @@ export async function Remove (
const context = parseContext(props.value)
if (context !== undefined) {
if (context.type === 'attribute') {
const addition = await getAttributeValue(control, execution, context)
const addition = getAttributeValue(control, execution, context)
return value.filter((item) => item !== addition)
}
} else {
@@ -115,13 +116,13 @@ export function Trim (value: string): string {
export async function Prepend (
value: string,
props: Record<string, string>,
control: TriggerControl,
control: ProcessControl,
execution: Execution
): Promise<string> {
const context = parseContext(props.value)
if (context !== undefined) {
if (context.type === 'attribute') {
const addition = await getAttributeValue(control, execution, context)
const addition = getAttributeValue(control, execution, context)
if (typeof addition !== 'string') return value
return addition + value
}
@@ -134,13 +135,13 @@ export async function Prepend (
export async function Append (
value: string,
props: Record<string, string>,
control: TriggerControl,
control: ProcessControl,
execution: Execution
): Promise<string> {
const context = parseContext(props.value)
if (context !== undefined) {
if (context.type === 'attribute') {
const addition = await getAttributeValue(control, execution, context)
const addition = getAttributeValue(control, execution, context)
if (typeof addition !== 'string') return value
return value + addition
}
@@ -210,13 +211,13 @@ export function Offset (val: Timestamp, props: Record<string, any>): Timestamp {
export async function Add (
value: number,
props: Record<string, any>,
control: TriggerControl,
control: ProcessControl,
execution: Execution
): Promise<number> {
const context = parseContext(props.value)
if (context !== undefined) {
if (context.type === 'attribute') {
const offset = await getAttributeValue(control, execution, context)
const offset = getAttributeValue(control, execution, context)
return value + offset
}
} else if (typeof value === 'number' && typeof props.value === 'number') {
@@ -228,13 +229,13 @@ export async function Add (
export async function Subtract (
value: number,
props: Record<string, any>,
control: TriggerControl,
control: ProcessControl,
execution: Execution
): Promise<number> {
const context = parseContext(props.value)
if (context !== undefined) {
if (context.type === 'attribute') {
const offset = await getAttributeValue(control, execution, context)
const offset = getAttributeValue(control, execution, context)
return value - offset
}
} else if (typeof value === 'number' && typeof props.value === 'number') {
@@ -246,13 +247,13 @@ export async function Subtract (
export async function Multiply (
value: number,
props: Record<string, any>,
control: TriggerControl,
control: ProcessControl,
execution: Execution
): Promise<number> {
const context = parseContext(props.value)
if (context !== undefined) {
if (context.type === 'attribute') {
const val = await getAttributeValue(control, execution, context)
const val = getAttributeValue(control, execution, context)
return value * val
}
} else if (typeof value === 'number' && typeof props.value === 'number') {
@@ -264,13 +265,13 @@ export async function Multiply (
export async function Divide (
value: number,
props: Record<string, any>,
control: TriggerControl,
control: ProcessControl,
execution: Execution
): Promise<number> {
const context = parseContext(props.value)
if (context !== undefined) {
if (context.type === 'attribute') {
const val = await getAttributeValue(control, execution, context)
const val = getAttributeValue(control, execution, context)
if (val === 0) {
return value // Avoid division by zero
}
@@ -288,13 +289,13 @@ export async function Divide (
export async function Modulo (
value: number,
props: Record<string, any>,
control: TriggerControl,
control: ProcessControl,
execution: Execution
): Promise<number> {
const context = parseContext(props.value)
if (context !== undefined) {
if (context.type === 'attribute') {
const val = await getAttributeValue(control, execution, context)
const val = getAttributeValue(control, execution, context)
if (val === 0) {
return value // Avoid division by zero
}
@@ -312,13 +313,13 @@ export async function Modulo (
export async function Power (
value: number,
props: Record<string, any>,
control: TriggerControl,
control: ProcessControl,
execution: Execution
): Promise<number> {
const context = parseContext(props.value)
if (context !== undefined) {
if (context.type === 'attribute') {
const val = await getAttributeValue(control, execution, context)
const val = getAttributeValue(control, execution, context)
return Math.pow(value, val)
}
} else if (typeof value === 'number' && typeof props.value === 'number') {
@@ -356,3 +357,19 @@ export function Floor (value: number): number {
}
// #endregion
// #region Func
export async function RoleContext (
value: null,
props: Record<string, any>,
control: ProcessControl,
execution: Execution
): Promise<Ref<Employee>[]> {
const targetRole = props.target
if (targetRole === undefined) return []
const users = await control.client.findAll(contact.class.UserRole, { role: targetRole })
return users.map((it) => it.user)
}
// #endregion
+16 -61
View File
@@ -13,67 +13,22 @@
// limitations under the License.
//
import { Doc, getObjectValue } from '@hcengineering/core'
import card from '@hcengineering/card'
import { getEmbeddedLabel, getResource } from '@hcengineering/platform'
import process, { Execution, SelectedContext, Transition, parseContext, processError } from '@hcengineering/process'
import { TriggerControl } from '@hcengineering/server-core'
import serverProcess from '@hcengineering/server-process'
import { getObjectValue } from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform'
import process, { Execution, SelectedContext, processError } from '@hcengineering/process'
import { ProcessControl } from '@hcengineering/server-process'
export async function pickTransition (
control: TriggerControl,
execution: Execution,
transitions: Transition[],
doc: Doc
): Promise<Transition | undefined> {
for (const tr of transitions) {
const trigger = control.modelDb.findObject(tr.trigger)
if (trigger === undefined) continue
if (trigger.checkFunction === undefined) return tr
const impl = control.hierarchy.as(trigger, serverProcess.mixin.TriggerImpl)
if (impl?.serverCheckFunc === undefined) return tr
const filled = fillParams(tr.triggerParams, execution)
const checkFunc = await getResource(impl.serverCheckFunc)
if (checkFunc === undefined) continue
const res = await checkFunc(filled, doc, control.hierarchy)
if (res) return tr
}
}
function fillParams (params: Record<string, any>, execution: Execution): Record<string, any> {
const res: Record<string, any> = {}
for (const key in params) {
const value = params[key]
const context = parseContext(value)
if (context === undefined) {
res[key] = value
continue
}
if (context.type === 'context') {
res[key] = execution.context[context.id]
}
}
return res
}
export async function getAttributeValue (
control: TriggerControl,
execution: Execution,
context: SelectedContext
): Promise<any> {
const cardValue = await control.findAll(control.ctx, card.class.Card, { _id: execution.card }, { limit: 1 })
if (cardValue.length > 0) {
const val = getObjectValue(context.key, cardValue[0])
if (val == null) {
const attr = control.hierarchy.findAttribute(cardValue[0]._class, context.key)
throw processError(
process.error.EmptyAttributeContextValue,
{},
{ attr: attr?.label ?? getEmbeddedLabel(context.key) }
)
}
return val
} else {
throw processError(process.error.ObjectNotFound, { _id: execution.card }, {}, true)
export function getAttributeValue (control: ProcessControl, execution: Execution, context: SelectedContext): any {
const card = control.cache.get(execution.card)
if (card === undefined) throw processError(process.error.ObjectNotFound, { _id: execution.card }, {}, true)
const val = getObjectValue(context.key, card)
if (val == null) {
const attr = control.client.getHierarchy().findAttribute(card._class, context.key)
throw processError(
process.error.EmptyAttributeContextValue,
{},
{ attr: attr?.label ?? getEmbeddedLabel(context.key) }
)
}
return val
}