fix(plugin-workflow): fix loop scope variable parsing (#2502)

* fix(plugin-workflow): fix loop scope variable parsing

* fix(plugin-workflow): fix api parameter

* fix(plugin-workflow): fix test cases
This commit is contained in:
Junyi 2023-08-22 19:13:52 +07:00 committed by GitHub
parent a625fc538d
commit 0fe5bdc859
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 58 additions and 78 deletions

View File

@ -318,7 +318,8 @@ export default class Processor {
return null; return null;
} }
public getScope(node?) { public getScope(sourceNodeId: number) {
const node = this.nodesMap.get(sourceNodeId);
const systemFns = {}; const systemFns = {};
const scope = { const scope = {
execution: this.execution, execution: this.execution,
@ -329,12 +330,10 @@ export default class Processor {
} }
const $scopes = {}; const $scopes = {};
if (node) { for (let n = this.findBranchParentNode(node); n; n = this.findBranchParentNode(n)) {
for (let n = this.findBranchParentNode(node); n; n = this.findBranchParentNode(n)) { const instruction = this.options.plugin.instructions.get(n.type);
const instruction = this.options.plugin.instructions.get(n.type); if (typeof instruction.getScope === 'function') {
if (typeof instruction.getScope === 'function') { $scopes[n.id] = instruction.getScope(n, this.jobsMapByNodeId[n.id], this);
$scopes[n.id] = instruction.getScope(n, this.jobsMapByNodeId[n.id], this);
}
} }
} }
@ -346,9 +345,9 @@ export default class Processor {
}; };
} }
public getParsedValue(value, node?, additionalScope?: object) { public getParsedValue(value, sourceNodeId: number, additionalScope?: object) {
const template = parse(value); const template = parse(value);
const scope = Object.assign(this.getScope(node), additionalScope); const scope = Object.assign(this.getScope(sourceNodeId), additionalScope);
template.parameters.forEach(({ key }) => { template.parameters.forEach(({ key }) => {
appendArrayColumn(scope, key); appendArrayColumn(scope, key);
}); });

View File

@ -29,7 +29,7 @@ describe('workflow > Processor', () => {
}); });
}); });
afterEach(() => db.close()); afterEach(() => app.destroy());
describe('base', () => { describe('base', () => {
it('empty workflow without any nodes', async () => { it('empty workflow without any nodes', async () => {

View File

@ -31,7 +31,7 @@ describe('workflow > instructions > aggregate', () => {
}); });
}); });
afterEach(() => db.close()); afterEach(() => app.destroy());
describe('based on collection', () => { describe('based on collection', () => {
it('count', async () => { it('count', async () => {

View File

@ -30,7 +30,7 @@ describe('workflow > instructions > calculation', () => {
}); });
}); });
afterEach(() => db.close()); afterEach(() => app.destroy());
describe('math.js', () => { describe('math.js', () => {
it('syntax error', async () => { it('syntax error', async () => {

View File

@ -28,7 +28,7 @@ describe('workflow > instructions > condition', () => {
}); });
}); });
afterEach(() => db.close()); afterEach(() => app.destroy());
describe('config.rejectOnFalse', () => {}); describe('config.rejectOnFalse', () => {});

View File

@ -29,7 +29,7 @@ describe('workflow > instructions > create', () => {
}); });
}); });
afterEach(() => db.close()); afterEach(() => app.destroy());
describe('create one', () => { describe('create one', () => {
it('params: from context', async () => { it('params: from context', async () => {

View File

@ -27,7 +27,7 @@ describe('workflow > instructions > destroy', () => {
}); });
}); });
afterEach(() => db.close()); afterEach(() => app.destroy());
describe('destroy one', () => { describe('destroy one', () => {
it('params: from context', async () => { it('params: from context', async () => {

View File

@ -49,7 +49,7 @@ describe('workflow > instructions > manual', () => {
}); });
}); });
afterEach(() => db.close()); afterEach(() => app.destroy());
describe('actions configuration', () => { describe('actions configuration', () => {
it('no action configured', async () => { it('no action configured', async () => {
@ -58,8 +58,7 @@ describe('workflow > instructions > manual', () => {
config: { config: {
assignees: [users[0].id], assignees: [users[0].id],
forms: { forms: {
f1: { f1: {},
},
}, },
}, },
}); });
@ -96,9 +95,7 @@ describe('workflow > instructions > manual', () => {
assignees: [users[0].id], assignees: [users[0].id],
forms: { forms: {
f1: { f1: {
actions: [ actions: [{ status: JOB_STATUS.RESOLVED, key: 'resolve' }],
{ status: JOB_STATUS.RESOLVED, key: 'resolve' },
],
}, },
}, },
}, },
@ -346,9 +343,7 @@ describe('workflow > instructions > manual', () => {
assignees: [users[0].id], assignees: [users[0].id],
forms: { forms: {
f1: { f1: {
actions: [ actions: [{ status: JOB_STATUS.RESOLVED, key: 'resolve' }],
{ status: JOB_STATUS.RESOLVED, key: 'resolve' },
],
}, },
}, },
}, },
@ -418,9 +413,7 @@ describe('workflow > instructions > manual', () => {
assignees: [users[0].id, users[1].id], assignees: [users[0].id, users[1].id],
forms: { forms: {
f1: { f1: {
actions: [ actions: [{ status: JOB_STATUS.RESOLVED, key: 'resolve' }],
{ status: JOB_STATUS.RESOLVED, key: 'resolve' },
]
}, },
}, },
}, },
@ -467,9 +460,7 @@ describe('workflow > instructions > manual', () => {
assignees: [users[0].id], assignees: [users[0].id],
forms: { forms: {
f1: { f1: {
actions: [ actions: [{ status: JOB_STATUS.RESOLVED, key: 'resolve' }],
{ status: JOB_STATUS.RESOLVED, key: 'resolve' },
],
}, },
}, },
}, },
@ -512,9 +503,7 @@ describe('workflow > instructions > manual', () => {
mode: 1, mode: 1,
forms: { forms: {
f1: { f1: {
actions: [ actions: [{ status: JOB_STATUS.RESOLVED, key: 'resolve' }],
{ status: JOB_STATUS.RESOLVED, key: 'resolve' },
],
}, },
}, },
}, },
@ -575,9 +564,7 @@ describe('workflow > instructions > manual', () => {
mode: 1, mode: 1,
forms: { forms: {
f1: { f1: {
actions: [ actions: [{ status: JOB_STATUS.REJECTED, key: 'reject' }],
{ status: JOB_STATUS.REJECTED, key: 'reject' },
],
}, },
}, },
}, },
@ -808,9 +795,7 @@ describe('workflow > instructions > manual', () => {
mode: -1, mode: -1,
forms: { forms: {
f1: { f1: {
actions: [ actions: [{ status: JOB_STATUS.REJECTED, key: 'reject' }],
{ status: JOB_STATUS.REJECTED, key: 'reject' },
],
}, },
}, },
}, },
@ -872,9 +857,7 @@ describe('workflow > instructions > manual', () => {
assignees: [users[0].id, users[1].id], assignees: [users[0].id, users[1].id],
forms: { forms: {
f1: { f1: {
actions: [ actions: [{ status: JOB_STATUS.RESOLVED, key: 'resolve' }],
{ status: JOB_STATUS.RESOLVED, key: 'resolve' },
],
}, },
}, },
}, },
@ -1013,9 +996,7 @@ describe('workflow > instructions > manual', () => {
forms: { forms: {
f1: { f1: {
type: 'create', type: 'create',
actions: [ actions: [{ status: JOB_STATUS.RESOLVED, key: 'resolve' }],
{ status: JOB_STATUS.RESOLVED, key: 'resolve' },
],
collection: 'comments', collection: 'comments',
}, },
}, },
@ -1129,9 +1110,7 @@ describe('workflow > instructions > manual', () => {
forms: { forms: {
f1: { f1: {
type: 'update', type: 'update',
actions: [ actions: [{ status: JOB_STATUS.RESOLVED, key: 'resolve' }],
{ status: JOB_STATUS.RESOLVED, key: 'resolve' },
],
collection: 'posts', collection: 'posts',
}, },
}, },

View File

@ -34,7 +34,7 @@ describe('workflow > instructions > query', () => {
}); });
}); });
afterEach(() => db.close()); afterEach(() => app.destroy());
describe('query one', () => { describe('query one', () => {
it('params: empty', async () => { it('params: empty', async () => {

View File

@ -27,7 +27,7 @@ describe('workflow > instructions > update', () => {
}); });
}); });
afterEach(() => db.close()); afterEach(() => app.destroy());
describe('update one', () => { describe('update one', () => {
it('params: from context', async () => { it('params: from context', async () => {

View File

@ -23,7 +23,7 @@ describe('workflow > triggers > collection', () => {
TagRepo = db.getCollection('tags').repository; TagRepo = db.getCollection('tags').repository;
}); });
afterEach(() => db.close()); afterEach(() => app.destroy());
describe('toggle', () => { describe('toggle', () => {
it('when collection change', async () => { it('when collection change', async () => {

View File

@ -15,12 +15,12 @@ const aggregators = {
export default { export default {
async run(node: FlowNodeModel, input, processor: Processor) { async run(node: FlowNodeModel, input, processor: Processor) {
const { aggregator, associated, collection, association = {}, params = {} } = node.config; const { aggregator, associated, collection, association = {}, params = {} } = node.config;
const options = processor.getParsedValue(params); const options = processor.getParsedValue(params, node.id);
const { database } = <typeof FlowNodeModel>node.constructor; const { database } = <typeof FlowNodeModel>node.constructor;
const repo = associated const repo = associated
? database.getRepository<HasManyRepository | BelongsToManyRepository>( ? database.getRepository<HasManyRepository | BelongsToManyRepository>(
`${association?.associatedCollection}.${association.name}`, `${association?.associatedCollection}.${association.name}`,
processor.getParsedValue(association?.associatedKey), processor.getParsedValue(association?.associatedKey, node.id),
) )
: database.getRepository(collection); : database.getRepository(collection);

View File

@ -15,7 +15,7 @@ export default {
async run(node: FlowNodeModel, prevJob, processor: Processor) { async run(node: FlowNodeModel, prevJob, processor: Processor) {
const { dynamic = false } = <CalculationConfig>node.config || {}; const { dynamic = false } = <CalculationConfig>node.config || {};
let { engine = 'math.js', expression = '' } = node.config; let { engine = 'math.js', expression = '' } = node.config;
let scope = processor.getScope(node); let scope = processor.getScope(node.id);
if (dynamic) { if (dynamic) {
const parsed = parse(dynamic)(scope) ?? {}; const parsed = parse(dynamic)(scope) ?? {};
engine = parsed.engine; engine = parsed.engine;

View File

@ -125,8 +125,8 @@ export default {
try { try {
result = evaluator result = evaluator
? evaluator(expression, processor.getScope()) ? evaluator(expression, processor.getScope(node.id))
: logicCalculate(processor.getParsedValue(calculation, node)); : logicCalculate(processor.getParsedValue(calculation, node.id));
} catch (e) { } catch (e) {
return { return {
result: e.toString(), result: e.toString(),

View File

@ -7,7 +7,7 @@ export default {
const { collection, params: { appends = [], ...params } = {} } = node.config; const { collection, params: { appends = [], ...params } = {} } = node.config;
const { repository, model } = (<typeof FlowNodeModel>node.constructor).database.getCollection(collection); const { repository, model } = (<typeof FlowNodeModel>node.constructor).database.getCollection(collection);
const options = processor.getParsedValue(params, node); const options = processor.getParsedValue(params, node.id);
const created = await repository.create({ const created = await repository.create({
...options, ...options,
context: { context: {

View File

@ -6,7 +6,7 @@ export default {
const { collection, params = {} } = node.config; const { collection, params = {} } = node.config;
const repo = (<typeof FlowNodeModel>node.constructor).database.getRepository(collection); const repo = (<typeof FlowNodeModel>node.constructor).database.getRepository(collection);
const options = processor.getParsedValue(params, node); const options = processor.getParsedValue(params, node.id);
const result = await repo.destroy({ const result = await repo.destroy({
...options, ...options,
context: { context: {

View File

@ -19,7 +19,7 @@ function getTargetLength(target) {
export default { export default {
async run(node: FlowNodeModel, prevJob: JobModel, processor: Processor) { async run(node: FlowNodeModel, prevJob: JobModel, processor: Processor) {
const [branch] = processor.getBranches(node); const [branch] = processor.getBranches(node);
const target = processor.getParsedValue(node.config.target, node); const target = processor.getParsedValue(node.config.target, node.id);
const length = getTargetLength(target); const length = getTargetLength(target);
if (!branch || !length) { if (!branch || !length) {
@ -61,7 +61,7 @@ export default {
const nextIndex = result + 1; const nextIndex = result + 1;
const target = processor.getParsedValue(loop.config.target, node); const target = processor.getParsedValue(loop.config.target, node.id);
// branchJob.status === JOB_STATUS.RESOLVED means branchJob is done, try next loop or exit as resolved // branchJob.status === JOB_STATUS.RESOLVED means branchJob is done, try next loop or exit as resolved
if (branchJob.status > JOB_STATUS.PENDING) { if (branchJob.status > JOB_STATUS.PENDING) {
job.set({ result: nextIndex }); job.set({ result: nextIndex });
@ -83,7 +83,7 @@ export default {
}, },
getScope(node, index, processor) { getScope(node, index, processor) {
const target = processor.getParsedValue(node.config.target, node); const target = processor.getParsedValue(node.config.target, node.id);
const targets = (Array.isArray(target) ? target : [target]).filter((t) => t != null); const targets = (Array.isArray(target) ? target : [target]).filter((t) => t != null);
const length = getTargetLength(target); const length = getTargetLength(target);
const item = typeof target === 'number' ? index : targets[index]; const item = typeof target === 'number' ? index : targets[index];

View File

@ -30,7 +30,7 @@ export async function submit(context: Context, next) {
} }
const { forms = {} } = userJob.node.config; const { forms = {} } = userJob.node.config;
const [formKey] = Object.keys(values.result ?? {}).filter(key => key !== '_'); const [formKey] = Object.keys(values.result ?? {}).filter((key) => key !== '_');
const actionKey = values.result?._; const actionKey = values.result?._;
const actionItem = forms[formKey]?.actions?.find((item) => item.key === actionKey); const actionItem = forms[formKey]?.actions?.find((item) => item.key === actionKey);
@ -40,7 +40,8 @@ export async function submit(context: Context, next) {
userJob.job.status !== JOB_STATUS.PENDING || userJob.job.status !== JOB_STATUS.PENDING ||
userJob.execution.status !== EXECUTION_STATUS.STARTED || userJob.execution.status !== EXECUTION_STATUS.STARTED ||
!userJob.workflow.enabled || !userJob.workflow.enabled ||
!actionKey || actionItem?.status == null !actionKey ||
actionItem?.status == null
) { ) {
return context.throw(400); return context.throw(400);
} }
@ -50,11 +51,11 @@ export async function submit(context: Context, next) {
await processor.prepare(); await processor.prepare();
// NOTE: validate assignee // NOTE: validate assignee
const assignees = processor.getParsedValue(userJob.node.config.assignees ?? []); const assignees = processor.getParsedValue(userJob.node.config.assignees ?? [], userJob.nodeId);
if (!assignees.includes(currentUser.id) || userJob.userId !== currentUser.id) { if (!assignees.includes(currentUser.id) || userJob.userId !== currentUser.id) {
return context.throw(403); return context.throw(403);
} }
const presetValues = processor.getParsedValue(actionItem.values ?? {}, null, { const presetValues = processor.getParsedValue(actionItem.values ?? {}, userJob.nodeId, {
currentUser: currentUser.toJSON(), currentUser: currentUser.toJSON(),
currentRecord: values.result[formKey], currentRecord: values.result[formKey],
currentTime: new Date(), currentTime: new Date(),
@ -62,9 +63,10 @@ export async function submit(context: Context, next) {
userJob.set({ userJob.set({
status: actionItem.status, status: actionItem.status,
result: actionItem.status > JOB_STATUS.PENDING result:
? { [formKey]: Object.assign(values.result[formKey], presetValues), _: actionKey } actionItem.status > JOB_STATUS.PENDING
: Object.assign(userJob.result ?? {}, values.result), ? { [formKey]: Object.assign(values.result[formKey], presetValues), _: actionKey }
: Object.assign(userJob.result ?? {}, values.result),
}); });
const handler = instruction.formTypes.get(forms[formKey].type); const handler = instruction.formTypes.get(forms[formKey].type);

View File

@ -10,7 +10,7 @@ export default async function (this: ManualInstruction, instance, { collection,
const { _, ...form } = instance.result; const { _, ...form } = instance.result;
const [values] = Object.values(form); const [values] = Object.values(form);
await repo.update({ await repo.update({
filter: processor.getParsedValue(filter), filter: processor.getParsedValue(filter, instance.nodeId),
values: { values: {
...((values as { [key: string]: any }) ?? {}), ...((values as { [key: string]: any }) ?? {}),
updatedBy: instance.userId, updatedBy: instance.userId,

View File

@ -2,7 +2,7 @@ import actions from '@nocobase/actions';
import { HandlerType } from '@nocobase/resourcer'; import { HandlerType } from '@nocobase/resourcer';
import { Registry } from '@nocobase/utils'; import { Registry } from '@nocobase/utils';
import Plugin from '../..'; import Plugin, { Processor } from '../..';
import { JOB_STATUS } from '../../constants'; import { JOB_STATUS } from '../../constants';
import { Instruction } from '..'; import { Instruction } from '..';
import jobsCollection from './collecions/jobs'; import jobsCollection from './collecions/jobs';
@ -124,9 +124,9 @@ export default class implements Instruction {
initFormTypes(this); initFormTypes(this);
} }
async run(node, prevJob, processor) { async run(node, prevJob, processor: Processor) {
const { mode, ...config } = node.config as ManualConfig; const { mode, ...config } = node.config as ManualConfig;
const assignees = [...new Set(processor.getParsedValue(config.assignees) || [])]; const assignees = [...new Set(processor.getParsedValue(config.assignees, node.id) || [])];
const job = await processor.saveJob({ const job = await processor.saveJob({
status: JOB_STATUS.PENDING, status: JOB_STATUS.PENDING,
@ -154,7 +154,7 @@ export default class implements Instruction {
return job; return job;
} }
async resume(node, job, processor) { async resume(node, job, processor: Processor) {
// NOTE: check all users jobs related if all done then continue as parallel // NOTE: check all users jobs related if all done then continue as parallel
const { assignees = [], mode } = node.config as ManualConfig; const { assignees = [], mode } = node.config as ManualConfig;

View File

@ -15,7 +15,7 @@ export default {
pageSize = DEFAULT_PER_PAGE, pageSize = DEFAULT_PER_PAGE,
sort = [], sort = [],
...options ...options
} = processor.getParsedValue(params, node); } = processor.getParsedValue(params, node.id);
const appends = options.appends const appends = options.appends
? Array.from( ? Array.from(
options.appends.reduce((set, field) => { options.appends.reduce((set, field) => {

View File

@ -52,7 +52,7 @@ export default class implements Instruction {
upstreamId: prevJob?.id ?? null, upstreamId: prevJob?.id ?? null,
}); });
const config = processor.getParsedValue(node.config, node) as RequestConfig; const config = processor.getParsedValue(node.config, node.id) as RequestConfig;
// eslint-disable-next-line promise/catch-or-return // eslint-disable-next-line promise/catch-or-return
request(config) request(config)

View File

@ -4,11 +4,11 @@ import type { FlowNodeModel } from '../types';
export default { export default {
async run(node: FlowNodeModel, input, processor: Processor) { async run(node: FlowNodeModel, input, processor: Processor) {
const { sequelize } = (<typeof FlowNodeModel>node.constructor).database; const { sequelize } = (<typeof FlowNodeModel>node.constructor).database;
const sql = processor.getParsedValue(node.config.sql ?? '', node).trim(); const sql = processor.getParsedValue(node.config.sql ?? '', node.id).trim();
if (!sql) { if (!sql) {
return { return {
status: JOB_STATUS.RESOLVED, status: JOB_STATUS.RESOLVED,
} };
} }
const result = await sequelize.query(sql, { const result = await sequelize.query(sql, {

View File

@ -7,7 +7,7 @@ export default {
const { collection, params = {} } = node.config; const { collection, params = {} } = node.config;
const repo = (<typeof FlowNodeModel>node.constructor).database.getRepository(collection); const repo = (<typeof FlowNodeModel>node.constructor).database.getRepository(collection);
const options = processor.getParsedValue(params, node); const options = processor.getParsedValue(params, node.id);
const result = await repo.update({ const result = await repo.update({
...options, ...options,
context: { context: {