From a845dc0937dbc9b5cf1dd1ae4359d8d7410927c2 Mon Sep 17 00:00:00 2001 From: Junyi Date: Wed, 25 Oct 2023 23:14:54 +0800 Subject: [PATCH] refactor(plugin-workflow): change to use node key for variables (#2909) * refactor(plugin-workflow): change to use node key for variables * refactor(plugin-workflow): add node key to node panel * fix(plugin-workflow): fix locale * fix(plugin-workflow): fix nodes variables * fix(plugin-workflow): fix migration * fix(plugin-workflow): fix migration and manual detail block --- .../src/client/ExecutionCanvas.tsx | 1 + .../src/client/components/ValueBlock.tsx | 7 +- .../src/client/nodes/aggregate.tsx | 4 +- .../src/client/nodes/calculation.tsx | 29 +------- .../src/client/nodes/create.tsx | 5 +- .../src/client/nodes/index.tsx | 32 ++++++++- .../plugin-workflow/src/client/nodes/loop.tsx | 2 +- .../nodes/manual/DetailsBlockProvider.tsx | 11 ++- .../src/client/nodes/manual/index.tsx | 6 +- .../src/client/nodes/query.tsx | 5 +- .../plugin-workflow/src/client/variable.tsx | 6 +- .../plugin-workflow/src/locale/zh-CN.ts | 1 + .../plugin-workflow/src/server/Processor.ts | 15 ++-- .../__tests__/actions/workflows.test.ts | 46 +----------- .../__tests__/instructions/aggregate.test.ts | 4 +- .../instructions/calculation.test.ts | 6 +- .../__tests__/instructions/manual.test.ts | 2 +- .../__tests__/instructions/query.test.ts | 2 +- .../__tests__/instructions/update.test.ts | 2 +- .../src/server/actions/workflows.ts | 70 +++++-------------- .../src/server/collections/flow_nodes.ts | 4 ++ .../src/server/instructions/index.ts | 5 +- .../20230221162902-jsonb-to-json.ts | 16 ----- .../20230809113132-workflow-options.ts | 7 +- .../migrations/20231024172342-add-node-key.ts | 65 +++++++++++++++++ .../src/server/triggers/index.ts | 5 +- 26 files changed, 178 insertions(+), 180 deletions(-) create mode 100644 packages/plugins/@nocobase/plugin-workflow/src/server/migrations/20231024172342-add-node-key.ts diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/ExecutionCanvas.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/ExecutionCanvas.tsx index c2f059a93..a9f39b39e 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/ExecutionCanvas.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/ExecutionCanvas.tsx @@ -32,6 +32,7 @@ function attachJobs(nodes, jobs: any[] = []): void { node.jobs.push(item); item.node = { id: node.id, + key: node.key, title: node.title, type: node.type, }; diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/components/ValueBlock.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/components/ValueBlock.tsx index d25239eb0..13639cebd 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/components/ValueBlock.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/components/ValueBlock.tsx @@ -32,7 +32,7 @@ function Initializer({ node, resultTitle, insert, ...props }) { 'x-component': 'ValueBlock.Result', 'x-component-props': { // NOTE: as same format as other reference for migration of revision - dataSource: `{{$jobsMapByNodeId.${node.id}}}`, + dataSource: `{{$jobsMapByNodeKey.${node.key}}}`, }, }, }, @@ -49,7 +49,10 @@ function Result({ dataSource }) { return field.title; } const result = parse(dataSource)({ - $jobsMapByNodeId: (execution.jobs ?? []).reduce((map, job) => Object.assign(map, { [job.nodeId]: job.result }), {}), + $jobsMapByNodeKey: (execution.jobs ?? []).reduce( + (map, job) => Object.assign(map, { [job.nodeId]: job.result }), + {}, + ), }); return ( diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/aggregate.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/aggregate.tsx index 4868250b8..36afbfa8e 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/aggregate.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/aggregate.tsx @@ -346,7 +346,7 @@ export default { ValueBlock, AssociatedConfig, }, - useVariables({ id, title }, { types, fieldNames = defaultFieldNames }) { + useVariables({ key, title }, { types, fieldNames = defaultFieldNames }) { if ( types && !types.some((type) => type in BaseTypeSets || Object.values(BaseTypeSets).some((set) => set.has(type))) @@ -354,7 +354,7 @@ export default { return null; } return { - [fieldNames.value]: `${id}`, + [fieldNames.value]: key, [fieldNames.label]: title, }; }, diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/calculation.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/calculation.tsx index 344037497..3b0e64029 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/calculation.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/calculation.tsx @@ -1,11 +1,9 @@ import { FormItem, FormLayout } from '@formily/antd-v5'; import { SchemaInitializerItemOptions, Variable, css, defaultFieldNames, useCollectionManager } from '@nocobase/client'; import { Evaluator, evaluators, getOptions } from '@nocobase/evaluators/client'; -import { parse } from '@nocobase/utils/client'; import { Radio } from 'antd'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useFlowContext } from '../FlowContext'; import { RadioWithTooltip } from '../components/RadioWithTooltip'; import { ValueBlock } from '../components/ValueBlock'; import { renderEngineReference } from '../components/renderEngineReference'; @@ -167,32 +165,11 @@ export default { }); return ; }, - CalculationResult({ dataSource }) { - const { execution } = useFlowContext(); - if (!execution) { - return lang('Calculation result'); - } - const result = parse(dataSource)({ - $jobsMapByNodeId: (execution.jobs ?? []).reduce( - (map, job) => Object.assign(map, { [job.nodeId]: job.result }), - {}, - ), - }); - - return ( -
-          {JSON.stringify(result, null, 2)}
-        
- ); - }, RadioWithTooltip, DynamicConfig, + ValueBlock, }, - useVariables({ id, title }, { types, fieldNames = defaultFieldNames }) { + useVariables({ key, title }, { types, fieldNames = defaultFieldNames }) { if ( types && !types.some((type) => type in BaseTypeSets || Object.values(BaseTypeSets).some((set) => set.has(type))) @@ -200,7 +177,7 @@ export default { return null; } return { - [fieldNames.value]: `${id}`, + [fieldNames.value]: key, [fieldNames.label]: title, }; }, diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/create.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/create.tsx index a9467097e..52b076e31 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/create.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/create.tsx @@ -44,13 +44,12 @@ export default { components: { CollectionFieldset, }, - useVariables({ id, title, config }, options) { + useVariables({ key: name, title, config }, options) { const compile = useCompile(); const { getCollectionFields } = useCollectionManager(); // const depth = config?.params?.appends?.length // ? config?.params?.appends.reduce((max, item) => Math.max(max, item.split('.').length), 1) // : 0; - const name = `${id}`; const [result] = getCollectionFieldOptions({ // collection: config.collection, // depth: options?.depth ?? depth, @@ -83,7 +82,7 @@ export default { title: node.title ?? `#${node.id}`, component: CollectionBlockInitializer, collection: node.config.collection, - dataSource: `{{$jobsMapByNodeId.${node.id}}}`, + dataSource: `{{$jobsMapByNodeKey.${node.key}}}`, }; }, initializers: {}, diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/index.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/index.tsx index 3115bbd66..6d26bbf43 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/index.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/index.tsx @@ -13,7 +13,7 @@ import { useResourceActionContext, } from '@nocobase/client'; import { Registry, parse, str2moment } from '@nocobase/utils/client'; -import { App, Button, Dropdown, Input, Tag, message } from 'antd'; +import { App, Button, Dropdown, Input, Tag, Tooltip, message } from 'antd'; import React, { useContext, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { AddButton } from '../AddButton'; @@ -199,7 +199,7 @@ export function RemoveButton() { const template = parse(node.config); const refs = template.parameters.filter( - ({ key }) => key.startsWith(`$jobsMapByNodeId.${current.id}.`) || key === `$jobsMapByNodeId.${current.id}`, + ({ key }) => key.startsWith(`$jobsMapByNodeKey.${current.key}.`) || key === `$jobsMapByNodeKey.${current.key}`, ); return refs.length; }); @@ -371,7 +371,33 @@ export function NodeDefaultView(props) { }, [`${instruction.type}_${data.id}`]: { type: 'void', - title: data.title, + title: ( +
+ {data.title} + + + {data.key} + + +
+ ), 'x-component': 'Action.Drawer', 'x-decorator': 'Form', 'x-decorator-props': { diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/loop.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/loop.tsx index fa048d54f..98043043a 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/loop.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/loop.tsx @@ -118,7 +118,7 @@ export default { // const current = useNodeContext(); // const upstreams = useAvailableUpstreams(current); // find target data model by path described in `config.target` - // 1. get options from $context/$jobsMapByNodeId + // 1. get options from $context/$jobsMapByNodeKey // 2. route to sub-options and use as loop target options let targetOption: VariableOption = { key: 'item', diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/DetailsBlockProvider.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/DetailsBlockProvider.tsx index fa69370de..ef3918278 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/DetailsBlockProvider.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/DetailsBlockProvider.tsx @@ -13,12 +13,17 @@ import { useFlowContext } from '../../FlowContext'; import { parse } from '@nocobase/utils/client'; function useFlowContextData(dataSource) { - const { execution } = useFlowContext(); + const { execution, nodes } = useFlowContext(); + + const nodesKeyMap = useMemo(() => { + return nodes.reduce((map, node) => Object.assign(map, { [node.id]: node.key }), {}); + }, [nodes]); + const data = useMemo( () => ({ $context: execution?.context, - $jobsMapByNodeId: (execution?.jobs ?? []).reduce( - (map, job) => Object.assign(map, { [job.nodeId]: job.result }), + $jobsMapByNodeKey: (execution?.jobs ?? []).reduce( + (map, job) => Object.assign(map, { [nodesKeyMap[job.nodeId]]: job.result }), {}, ), }), diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/index.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/index.tsx index b9b31acd2..445da407e 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/index.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/index.tsx @@ -85,7 +85,7 @@ export default { ModeConfig, AssigneesSelect, }, - useVariables({ id, title, config }, { types, fieldNames = defaultFieldNames }) { + useVariables({ key, title, config }, { types, fieldNames = defaultFieldNames }) { const compile = useCompile(); const { getCollectionFields } = useCollectionManager(); const formKeys = Object.keys(config.forms ?? {}); @@ -119,7 +119,7 @@ export default { return options.length ? { - [fieldNames.value]: `${id}`, + [fieldNames.value]: key, [fieldNames.label]: title, [fieldNames.children]: options, } @@ -143,7 +143,7 @@ export default { title: form.title ?? formKey, component: CollectionBlockInitializer, collection: form.collection, - dataSource: `{{$jobsMapByNodeId.${node.id}.${formKey}}}`, + dataSource: `{{$jobsMapByNodeKey.${node.key}.${formKey}}}`, } as SchemaInitializerItemOptions) : null; }) diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/query.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/query.tsx index f488eed3d..70b2021ce 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/query.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/query.tsx @@ -89,13 +89,12 @@ export default { FilterDynamicComponent, SchemaComponentContext, }, - useVariables({ id, title, config }, options) { + useVariables({ key: name, title, config }, options) { const compile = useCompile(); const { getCollectionFields } = useCollectionManager(); // const depth = config?.params?.appends?.length // ? config?.params?.appends.reduce((max, item) => Math.max(max, item.split('.').length), 1) // : 0; - const name = `${id}`; const [result] = getCollectionFieldOptions({ // collection: config.collection, // depth: options?.depth ?? depth, @@ -128,7 +127,7 @@ export default { title: node.title ?? `#${node.id}`, component: CollectionBlockInitializer, collection: node.config.collection, - dataSource: `{{$jobsMapByNodeId.${node.id}}}`, + dataSource: `{{$jobsMapByNodeKey.${node.key}}}`, }; }, initializers: {}, diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/variable.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/variable.tsx index b0d5c276c..834b65bd9 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/variable.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/variable.tsx @@ -37,7 +37,7 @@ export const defaultFieldNames = { label: 'label', value: 'value', children: 'ch export const nodesOptions = { label: `{{t("Node result", { ns: "${NAMESPACE}" })}}`, - value: '$jobsMapByNodeId', + value: '$jobsMapByNodeKey', useOptions(options: OptionsOfUseVariableOptions) { const current = useNodeContext(); const upstreams = useAvailableUpstreams(current); @@ -76,8 +76,8 @@ export const scopeOptions = { const subOptions = instruction.useScopeVariables?.(node, options); if (subOptions) { result.push({ - key: node.id.toString(), - [fieldNames.value]: node.id.toString(), + key: node.key, + [fieldNames.value]: node.key, [fieldNames.label]: node.title ?? `#${node.id}`, [fieldNames.children]: subOptions, }); diff --git a/packages/plugins/@nocobase/plugin-workflow/src/locale/zh-CN.ts b/packages/plugins/@nocobase/plugin-workflow/src/locale/zh-CN.ts index 3eea9b744..45c6584a1 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/locale/zh-CN.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/locale/zh-CN.ts @@ -89,6 +89,7 @@ export default { Advanced: '高级模式', End: '结束', 'Node result': '节点数据', + 'Variable key of node': '节点变量标识', Calculator: '运算', 'Calculate an expression based on a calculation engine and obtain a value as the result. Variables in the upstream nodes can be used in the expression. The expression can be static or dynamic one from an expression collections.': '基于计算引擎对一个表达式进行计算,并获得一个值作为结果。表达式中可以使用上游节点里的变量。表达式可以是静态的,也可以是表达式表中的动态表达式。', diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/Processor.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/Processor.ts index ab953e27e..73b918c19 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/Processor.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/Processor.ts @@ -30,7 +30,7 @@ export default class Processor { nodes: FlowNodeModel[] = []; nodesMap = new Map(); jobsMap = new Map(); - jobsMapByNodeId: { [key: number]: any } = {}; + jobsMapByNodeKey: { [key: string]: any } = {}; constructor( public execution: ExecutionModel, @@ -61,8 +61,9 @@ export default class Processor { private makeJobs(jobs: Array) { jobs.forEach((job) => { this.jobsMap.set(job.id, job); - // TODO: should consider cycle, and from previous job - this.jobsMapByNodeId[job.nodeId] = job.result; + + const node = this.nodesMap.get(job.nodeId); + this.jobsMapByNodeKey[node.key] = job.result; }); } @@ -252,7 +253,9 @@ export default class Processor { ); } this.jobsMap.set(job.id, job); - this.jobsMapByNodeId[job.nodeId] = job.result; + + const node = this.nodesMap.get(job.nodeId); + this.jobsMapByNodeKey[node.key] = job.result; return job; } @@ -339,13 +342,13 @@ export default class Processor { for (let n = this.findBranchParentNode(node); n; n = this.findBranchParentNode(n)) { const instruction = this.options.plugin.instructions.get(n.type); if (typeof instruction.getScope === 'function') { - $scopes[n.id] = instruction.getScope(n, this.jobsMapByNodeId[n.id], this); + $scopes[n.id] = $scopes[n.key] = instruction.getScope(n, this.jobsMapByNodeKey[n.key], this); } } return { $context: this.execution.context, - $jobsMapByNodeId: this.jobsMapByNodeId, + $jobsMapByNodeKey: this.jobsMapByNodeKey, $system: systemFns, $scopes, }; diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/actions/workflows.test.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/actions/workflows.test.ts index d24f221fe..53b28c0c5 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/actions/workflows.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/actions/workflows.test.ts @@ -260,7 +260,7 @@ describe('workflow > actions > workflows', () => { type: 'calculation', config: { engine: 'math.js', - expression: `{{$jobsMapByNodeId.${n1.id}.data.read}} + {{$jobsMapByNodeId.${n1.id}.data.read}}`, + expression: `{{$jobsMapByNodeKey.${n1.key}.data.read}} + {{$jobsMapByNodeKey.${n1.key}.data.read}}`, }, upstreamId: n1.id, }); @@ -279,11 +279,12 @@ describe('workflow > actions > workflows', () => { const n1_2 = w2.nodes.find((n) => !n.upstreamId); const n2_2 = w2.nodes.find((n) => !n.downstreamId); + expect(n1_2.key).toBe(n1.key); expect(n1_2.type).toBe('echo'); expect(n2_2.type).toBe('calculation'); expect(n2_2.config).toMatchObject({ engine: 'math.js', - expression: `{{$jobsMapByNodeId.${n1_2.id}.data.read}} + {{$jobsMapByNodeId.${n1_2.id}.data.read}}`, + expression: `{{$jobsMapByNodeKey.${n1_2.key}.data.read}} + {{$jobsMapByNodeKey.${n1_2.key}.data.read}}`, }); await w2.update({ enabled: true }); @@ -299,47 +300,6 @@ describe('workflow > actions > workflows', () => { expect(calculation.result).toBe(2); }); - it('revision with using of deleted nodes', async () => { - const w1 = await WorkflowModel.create({ - enabled: true, - type: 'collection', - config: { - mode: 1, - collection: 'posts', - }, - }); - - const n2 = await w1.createNode({ - type: 'calculation', - config: { - calculation: { - calculator: 'add', - operands: [ - { - type: '$jobsMapByNodeId', - options: { - nodeId: 0, - path: 'data.read', - }, - }, - { - value: `{{$jobsMapByNodeId.0.data.read}}`, - }, - ], - }, - }, - }); - - const { status } = await agent.resource(`workflows`).revision({ - filterByTk: w1.id, - filter: { - key: w1.key, - }, - }); - - expect(status).toBe(400); - }); - it('duplicate workflow', async () => { const w1 = await WorkflowModel.create({ enabled: true, diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/instructions/aggregate.test.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/instructions/aggregate.test.ts index a83a9b153..5b67c9523 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/instructions/aggregate.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/instructions/aggregate.test.ts @@ -248,7 +248,7 @@ describe('workflow > instructions > aggregate', () => { associated: true, association: { name: 'posts', - associatedKey: `{{$jobsMapByNodeId.${n1.id}.id}}`, + associatedKey: `{{$jobsMapByNodeKey.${n1.key}.id}}`, associatedCollection: 'tags', }, params: { @@ -266,7 +266,7 @@ describe('workflow > instructions > aggregate', () => { associated: true, association: { name: 'posts', - associatedKey: `{{$jobsMapByNodeId.${n1.id}.id}}`, + associatedKey: `{{$jobsMapByNodeKey.${n1.key}.id}}`, associatedCollection: 'tags', }, params: { diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/instructions/calculation.test.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/instructions/calculation.test.ts index 5561c0c5a..d9c7cf433 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/instructions/calculation.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/instructions/calculation.test.ts @@ -88,7 +88,7 @@ describe('workflow > instructions > calculation', () => { expect(job.result).toBe(2); }); - it('$jobsMapByNodeId', async () => { + it('$jobsMapByNodeKey', async () => { const n1 = await workflow.createNode({ type: 'echo', }); @@ -97,7 +97,7 @@ describe('workflow > instructions > calculation', () => { type: 'calculation', config: { engine: 'math.js', - expression: `{{$jobsMapByNodeId.${n1.id}.data.read}} + 1`, + expression: `{{$jobsMapByNodeKey.${n1.key}.data.read}} + 1`, }, upstreamId: n1.id, }); @@ -231,7 +231,7 @@ describe('workflow > instructions > calculation', () => { const n2 = await workflow.createNode({ type: 'calculation', config: { - dynamic: `{{$jobsMapByNodeId.${n1.id}}}`, + dynamic: `{{$jobsMapByNodeKey.${n1.key}}}`, scope: '{{$context.data}}', }, upstreamId: n1.id, diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/instructions/manual.test.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/instructions/manual.test.ts index 50105c1a2..f2699c301 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/instructions/manual.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/instructions/manual.test.ts @@ -867,7 +867,7 @@ describe('workflow > instructions > manual', () => { type: 'calculation', config: { engine: 'math.js', - expression: `{{$jobsMapByNodeId.${n1.id}.f1.number}} + 1`, + expression: `{{$jobsMapByNodeKey.${n1.key}.f1.number}} + 1`, }, upstreamId: n1.id, }); diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/instructions/query.test.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/instructions/query.test.ts index 72b3ef4f3..c1d1eb413 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/instructions/query.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/instructions/query.test.ts @@ -130,7 +130,7 @@ describe('workflow > instructions > query', () => { collection: 'posts', params: { filter: { - title: `{{$jobsMapByNodeId.${n1.id}.data.title}}`, + title: `{{$jobsMapByNodeKey.${n1.key}.data.title}}`, }, }, }, diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/instructions/update.test.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/instructions/update.test.ts index 0ef52a0cb..c20485afc 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/instructions/update.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/instructions/update.test.ts @@ -78,7 +78,7 @@ describe('workflow > instructions > update', () => { collection: 'posts', params: { filter: { - id: `{{$jobsMapByNodeId.${n1.id}.id}}`, + id: `{{$jobsMapByNodeKey.${n1.key}.id}}`, }, values: { title: 'changed', diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/actions/workflows.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/actions/workflows.ts index a67a237d1..64acbd11e 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/actions/workflows.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/actions/workflows.ts @@ -1,6 +1,8 @@ import actions, { Context, utils } from '@nocobase/actions'; import { Op, Repository } from '@nocobase/database'; +import Plugin from '../Plugin'; + export async function update(context: Context, next) { const repository = utils.getRepositoryFromParams(context) as Repository; const { filterByTk, values } = context.action.params; @@ -51,51 +53,8 @@ export async function destroy(context: Context, next) { next(); } -function typeOf(value) { - if (Array.isArray(value)) { - return 'array'; - } else if (value instanceof Date) { - return 'date'; - } else if (value === null) { - return 'null'; - } - - return typeof value; -} - -function migrateConfig(config, oldToNew) { - function migrate(value) { - switch (typeOf(value)) { - case 'object': - if (value.type === '$jobsMapByNodeId') { - return { - ...value, - options: { - ...value.options, - nodeId: oldToNew.get(value.options?.nodeId)?.id, - }, - }; - } - return Object.keys(value).reduce((result, key) => ({ ...result, [key]: migrate(value[key]) }), {}); - case 'array': - return value.map((item) => migrate(item)); - case 'string': - return value.replace(/({{\$jobsMapByNodeId|{{\$scopes)\.([\w-]+)/g, (_, jobVar, oldNodeId) => { - const newNode = oldToNew.get(Number.parseInt(oldNodeId, 10)); - if (!newNode) { - throw new Error('node configurated for result is not existed'); - } - return `${jobVar}.${newNode.id}`; - }); - default: - return value; - } - } - - return migrate(config); -} - export async function revision(context: Context, next) { + const plugin = context.app.getPlugin('workflow') as Plugin; const { db } = context; const repository = utils.getRepositoryFromParams(context); const { filterByTk, filter = {}, values = {} } = context.action.params; @@ -109,6 +68,8 @@ export async function revision(context: Context, next) { transaction, }); + const trigger = plugin.triggers.get(origin.type); + const revisionData = filter.key ? { key: filter.key, @@ -121,9 +82,12 @@ export async function revision(context: Context, next) { values: { title: `${origin.title} copy`, description: origin.description, - type: origin.type, - config: origin.config, ...revisionData, + type: origin.type, + config: + typeof trigger.duplicateConfig === 'function' + ? await trigger.duplicateConfig(origin, { transaction }) + : origin.config, }, transaction, }); @@ -136,10 +100,15 @@ export async function revision(context: Context, next) { const oldToNew = new Map(); const newToOld = new Map(); for await (const node of origin.nodes) { + const instruction = plugin.instructions.get(node.type); const newNode = await instance.createNode( { type: node.type, - config: node.config, + key: node.key, + config: + typeof instruction.duplicateConfig === 'function' + ? await instruction.duplicateConfig(node, { transaction }) + : node.config, title: node.title, branchIndex: node.branchIndex, }, @@ -154,18 +123,11 @@ export async function revision(context: Context, next) { const oldNode = originalNodesMap.get(oldId); const newUpstream = oldNode.upstreamId ? oldToNew.get(oldNode.upstreamId) : null; const newDownstream = oldNode.downstreamId ? oldToNew.get(oldNode.downstreamId) : null; - let migratedConfig; - try { - migratedConfig = migrateConfig(oldNode.config, oldToNew); - } catch (err) { - return context.throw(400, err.message); - } await newNode.update( { upstreamId: newUpstream?.id ?? null, downstreamId: newDownstream?.id ?? null, - config: migratedConfig, }, { transaction }, ); diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/collections/flow_nodes.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/collections/flow_nodes.ts index 2fae8712a..62dadd794 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/collections/flow_nodes.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/collections/flow_nodes.ts @@ -5,6 +5,10 @@ export default { duplicator: 'required', name: 'flow_nodes', fields: [ + { + type: 'uid', + name: 'key', + }, { type: 'string', name: 'title', diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/instructions/index.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/instructions/index.ts index b8cfcefdd..4ddec04f1 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/instructions/index.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/instructions/index.ts @@ -1,6 +1,7 @@ import path from 'path'; import { requireModule } from '@nocobase/utils'; +import { Transactionable } from '@nocobase/database'; import Plugin from '..'; import Processor from '../Processor'; @@ -25,7 +26,9 @@ export interface Instruction { // for start node in main flow (or branch) to resume when manual sub branch triggered resume?: Runner; - getScope?: (node: FlowNodeModel, job: any, processor: Processor) => any; + getScope?: (node: FlowNodeModel, data: any, processor: Processor) => any; + + duplicateConfig?: (node: FlowNodeModel, options: Transactionable) => object | Promise; } type InstructionConstructor = { new (p: Plugin): T }; diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/migrations/20230221162902-jsonb-to-json.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/migrations/20230221162902-jsonb-to-json.ts index cc06aa4ba..e36697b4f 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/migrations/20230221162902-jsonb-to-json.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/migrations/20230221162902-jsonb-to-json.ts @@ -30,22 +30,6 @@ export default class extends Migration { }, { transaction }, ); - await queryInterface.changeColumn( - db.getCollection('executions').model.getTableName(), - 'context', - { - type: DataTypes.JSON, - }, - { transaction }, - ); - await queryInterface.changeColumn( - db.getCollection('jobs').model.getTableName(), - 'result', - { - type: DataTypes.JSON, - }, - { transaction }, - ); }); } } diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/migrations/20230809113132-workflow-options.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/migrations/20230809113132-workflow-options.ts index 57fc44f44..53afd6b56 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/migrations/20230809113132-workflow-options.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/migrations/20230809113132-workflow-options.ts @@ -2,12 +2,12 @@ import { Migration } from '@nocobase/server'; export default class extends Migration { async up() { - const match = await this.app.version.satisfies('<0.11.0-alpha.2'); + const match = await this.app.version.satisfies('<0.14.0-alpha.8'); if (!match) { return; } const { db } = this.context; - const WorkflowRepo = db.getRepository('flow_nodes'); + const WorkflowRepo = db.getRepository('workflows'); await db.sequelize.transaction(async (transaction) => { const workflows = await WorkflowRepo.find({ transaction, @@ -16,6 +16,9 @@ export default class extends Migration { await workflows.reduce( (promise, workflow) => promise.then(() => { + if (!workflow.useTransaction) { + return; + } workflow.set('options', { useTransaction: workflow.get('useTransaction'), }); diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/migrations/20231024172342-add-node-key.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/migrations/20231024172342-add-node-key.ts new file mode 100644 index 000000000..b103dbbe0 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/migrations/20231024172342-add-node-key.ts @@ -0,0 +1,65 @@ +import { DataTypes } from 'sequelize'; +import { Migration } from '@nocobase/server'; +import { uid } from '@nocobase/utils'; + +function migrateNodeConfig(config = {}, nodesMap) { + Object.keys(config).forEach((key) => { + const valueType = typeof config[key]; + if (valueType === 'string') { + config[key] = config[key] + .replace(/{{\s*\$jobsMapByNodeId\.(\d+)(\.[^}]+)?\s*}}/g, (matched, id, path) => { + return `{{$jobsMapByNodeKey.${nodesMap[id].key}${path || ''}}}`; + }) + .replace(/{{\s*\$scopes\.(\d+)(\.[^}]+)?\s*}}/g, (matched, id, path) => { + return `{{$scopes.${nodesMap[id].key}${path || ''}}}`; + }); + } else if (valueType === 'object' && config[key]) { + migrateNodeConfig(config[key], nodesMap); + } + }); + return config; +} + +export default class extends Migration { + async up() { + const match = await this.app.version.satisfies('<0.14.0-alpha.8'); + if (!match) { + return; + } + const { db } = this.context; + + const NodeRepo = db.getRepository('flow_nodes'); + const { key } = await this.queryInterface.describeTable('flow_nodes'); + await db.sequelize.transaction(async (transaction) => { + if (!key) { + await this.queryInterface.addColumn('flow_nodes', 'key', DataTypes.STRING, { + transaction, + }); + } + const nodes = await NodeRepo.find({ + transaction, + }); + + const nodesMap = nodes.reduce((map, node) => { + map[node.id] = node; + if (!node.get('key')) { + node.set('key', uid()); + } + return map; + }, {}); + + await nodes.reduce( + (promise, node) => + promise.then(() => { + node.set('config', migrateNodeConfig(node.config, nodesMap)); + node.changed('config', true); + return node.save({ + silent: true, + transaction, + }); + }), + Promise.resolve(), + ); + }); + } +} diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/index.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/index.ts index 35bab363d..698b3e5f3 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/index.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/index.ts @@ -1,6 +1,8 @@ -import { requireModule } from '@nocobase/utils'; import path from 'path'; +import { requireModule } from '@nocobase/utils'; +import { Transactionable } from '@nocobase/database'; + import Plugin from '..'; import type { WorkflowModel } from '../types'; @@ -8,6 +10,7 @@ export abstract class Trigger { constructor(public readonly plugin: Plugin) {} abstract on(workflow: WorkflowModel): void; abstract off(workflow: WorkflowModel): void; + duplicateConfig?(workflow: WorkflowModel, options: Transactionable): object | Promise; } export default function (plugin, more: { [key: string]: { new (p: Plugin): T } } = {}) {