mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-22 01:25:00 +02:00
Extract processor (#9614)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
@@ -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() === '')
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user