Files
huly-platform/server-plugins/process-resources/src/index.ts
T

1071 lines
38 KiB
TypeScript

//
// 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 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 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 {
Absolute,
Add,
All,
Append,
Ceil,
Cut,
Divide,
FirstValue,
FirstWorkingDayAfter,
Floor,
Insert,
LastValue,
LowerCase,
Modulo,
Multiply,
Offset,
Power,
Prepend,
Random,
Remove,
Replace,
ReplaceAll,
Round,
Split,
Subtract,
RemoveFirst,
RemoveLast,
Trim,
UpperCase
} from './transform'
import { getAttributeValue, pickTransition } from './utils'
export async function OnProcessToDoClose (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<ProcessToDo>
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
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
}
}
} 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
}
}
}
}
}
return res
}
export async function OnExecutionCreate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = []
for (const tx of txes) {
if (tx._class !== core.class.TxCreateDoc) continue
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)))
}
return res
}
export async function OnProcessToDoRemove (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = []
for (const tx of txes) {
if (tx._class !== core.class.TxRemoveDoc) continue
const removeTx = tx as TxRemoveDoc<ProcessToDo>
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)
}
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)))
}
}
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) {
if (tx._class !== core.class.TxUpdateDoc) continue
if (tx.space !== core.space.Tx) continue
const updateTx = tx as TxUpdateDoc<Execution>
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
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, [])))
}
}
return res
}
export async function OnProcessRemove (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = []
for (const tx of txes) {
if (tx._class !== core.class.TxRemoveDoc) continue
const removeTx = tx as TxRemoveDoc<Process>
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<Tx[]> {
const res: Tx[] = []
for (const tx of txes) {
if (tx._class !== core.class.TxRemoveDoc) continue
const removeTx = tx as TxRemoveDoc<State>
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 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) {
if (!control.hierarchy.isDerived(tx._class, core.class.TxCUD)) continue
const cudTx = tx as TxCUD<Transition>
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<Tx[]> {
const res: Tx[] = []
for (const tx of txes) {
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, [])))
}
}
return res
}
async function syncContext (control: TriggerControl, _process: Process): Promise<Tx | undefined> {
const transitions = control.modelDb.findAllSync(process.class.Transition, { process: _process._id })
const exists = new Set<ContextId>()
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?.contextClass != null) {
changed = true
const ctx: SelectedExecutonContext = {
type: 'context',
id: action.context._id,
key: ''
}
_process.context[action.context._id] = {
name: current?.name ?? '',
_class: action.context._class ?? method.contextClass,
action: action._id,
index: index++,
producer: transition._id,
value: ctx
}
}
}
if (action.result?._id != null) {
exists.add(action.result._id)
const context = _process.context[action.result._id]
changed = true
const ctx: SelectedExecutonContext = {
type: 'context',
id: action.result._id,
key: ''
}
_process.context[action.result._id] = {
name: context?.name ?? action.result.name,
isResult: true,
type: action.result.type,
_class: action.result.type._class,
action: action._id,
index: index++,
producer: transition._id,
value: ctx
}
}
}
}
const newContext: Record<ContextId, ProcessContext> = {}
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
})
}
}
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: {
RunSubProcess,
CreateToDo,
UpdateCard,
CreateCard,
AddRelation,
CheckToDo,
OnCardUpdateCheck
},
transform: {
FirstValue,
LastValue,
Random,
All,
UpperCase,
LowerCase,
Trim,
Prepend,
Append,
Replace,
ReplaceAll,
Split,
Cut,
Add,
Subtract,
Multiply,
Divide,
Modulo,
Power,
Round,
Absolute,
Ceil,
Floor,
Offset,
FirstWorkingDayAfter,
RoleContext,
Insert,
Remove,
RemoveFirst,
RemoveLast
},
trigger: {
OnProcessRemove,
OnStateRemove,
OnTransition,
OnCardUpdate,
OnExecutionCreate,
OnExecutionTransition,
OnProcessToDoClose,
OnProcessToDoRemove,
OnExecutionContinue
}
})