feat/js-parse (#1097)

Reviewed-on: daoyoucloud/tachybase#1097
Co-authored-by: bai.zixv <bai.zixv@foxmail.com>
Co-committed-by: bai.zixv <bai.zixv@foxmail.com>
This commit is contained in:
bai.zixv 2024-05-31 20:27:27 +08:00 committed by baizixv
parent c8b5005bd7
commit 7f15916a19
8 changed files with 238 additions and 0 deletions

View File

@ -0,0 +1,146 @@
import { css } from '@tachybase/client';
import { ArrayTable } from '@tachybase/components';
import { VariableOption, WorkflowVariableInput } from '../..';
import { NAMESPACE_INSTRUCTION_JS_PARSE } from '../../../common/constants';
import { tval } from '../../locale';
import { Instruction } from '../../nodes';
export class JSParseInstruction extends Instruction {
title = tval('JS Parse');
type = NAMESPACE_INSTRUCTION_JS_PARSE;
// XXX: 这里应该定义在 workflow 里的一个统一的地方. workflow 本身没处理好这块, 先这样直接写了.
group = 'extended';
description = tval('Get specific data from JSON result of any node BY js code;');
fieldset = {
source: {
type: 'string',
title: tval('Data source'),
'x-decorator': 'FormItem',
'x-component': 'WorkflowVariableInput',
'x-component-props': {
changeOnSelect: true,
},
required: true,
},
JSCode: {
type: 'string',
title: tval('JSCode expression'),
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
required: true,
},
model: {
type: 'array',
title: tval('Properties mapping'),
description: tval(
'If the type of query result is object or array of object, could map the properties which to be accessed in subsequent nodes.',
),
'x-decorator': 'FormItem',
'x-component': 'ArrayTable',
items: {
type: 'object',
properties: {
path: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': {
title: tval('Property path'),
},
properties: {
path: {
type: 'string',
name: 'path',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
alias: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': {
title: tval('Alias'),
},
properties: {
alias: {
type: 'string',
name: 'alias',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
label: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': {
title: tval('Label'),
},
properties: {
label: {
type: 'string',
name: 'label',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
operations: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': {
dataIndex: 'operations',
fixed: 'right',
className: css`
> *:not(:last-child) {
margin-right: 0.5em;
}
button {
padding: 0;
}
`,
},
properties: {
remove: {
type: 'void',
'x-component': 'ArrayTable.Remove',
},
},
},
},
},
properties: {
add: {
type: 'void',
title: tval('Add property'),
'x-component': 'ArrayTable.Addition',
'x-component-props': {
defaultValue: {},
},
},
},
},
};
components = {
ArrayTable,
WorkflowVariableInput,
};
useVariables(node, options): VariableOption {
const { key, title, config } = node;
const { types, fieldNames } = options;
const model = config.model || [];
const result = {
[fieldNames.label]: title,
[fieldNames.value]: key,
[fieldNames.children]: model.map((item) => ({
[fieldNames.label]: item.label,
[fieldNames.value]: item.alias || item.path,
})),
};
return result;
}
}

View File

@ -0,0 +1,14 @@
import { Plugin } from '@tachybase/client';
import PluginWorkflow from '../..';
import { NAMESPACE_INSTRUCTION_JS_PARSE } from '../../../common/constants';
import { JSParseInstruction } from './JSParse.instruction';
export class PluginWorkflowJSParseClient extends Plugin {
async load() {
const pluginWorkflow = this.app.pm.get(PluginWorkflow);
pluginWorkflow.registerInstruction(NAMESPACE_INSTRUCTION_JS_PARSE, JSParseInstruction);
}
}
export default PluginWorkflowJSParseClient;

View File

@ -7,6 +7,7 @@ import { PluginActionTrigger } from './features/action-trigger';
import { PluginAggregate } from './features/aggregate';
import { PluginDelay } from './features/delay';
import { PluginDaynamicCalculation } from './features/dynamic-calculation';
import PluginWorkflowJSParseClient from './features/js-parse';
import PluginWorkflowJsonParseClient from './features/json-parse';
import { PluginLoop } from './features/loop';
import { PluginManual } from './features/manual';
@ -77,6 +78,7 @@ export default class PluginWorkflowClient extends Plugin {
await this.pm.add(PluginAggregate);
await this.pm.add(PluginActionTrigger);
await this.pm.add(PluginWorkflowJsonParseClient);
await this.pm.add(PluginWorkflowJSParseClient);
}
async load() {

View File

@ -1 +1,2 @@
export const NAMESPACE_INSTRUCTION_JSON_PARSE = 'json-parse';
export const NAMESPACE_INSTRUCTION_JS_PARSE = 'js-parse';

View File

@ -12,6 +12,7 @@ import { PluginActionTrigger } from './features/action-trigger/Plugin';
import { PluginAggregate } from './features/aggregate/Plugin';
import { PluginDelay } from './features/delay/Plugin';
import { PluginDynamicCalculation } from './features/dynamic-calculation/Plugin';
import PluginWorkflowJSParseServer from './features/js-parse/plugin';
import PluginWorkflowJSONParseServer from './features/json-parse/plugin';
import { PluginLoop } from './features/loop/Plugin';
import { PluginManual } from './features/manual/Plugin';
@ -64,6 +65,7 @@ export default class PluginWorkflowServer extends Plugin {
pluginAggregate: PluginAggregate;
pluginActionTrigger: PluginActionTrigger;
pluginJSONParse: PluginWorkflowJSONParseServer;
pluginJSParse: PluginWorkflowJSParseServer;
constructor(app: Application, options?: PluginOptions) {
super(app, options);
@ -77,6 +79,7 @@ export default class PluginWorkflowServer extends Plugin {
this.pluginAggregate = new PluginAggregate(app, options);
this.pluginActionTrigger = new PluginActionTrigger(app, options);
this.pluginJSONParse = new PluginWorkflowJSONParseServer(app, options);
this.pluginJSParse = new PluginWorkflowJSParseServer(app, options);
}
getLogger(workflowId: ID): Logger {
@ -301,6 +304,7 @@ export default class PluginWorkflowServer extends Plugin {
await this.pluginParallel.load();
await this.pluginRequest.load();
await this.pluginJSONParse.load();
await this.pluginJSParse.load();
}
toggle(workflow: WorkflowModel, enable?: boolean) {

View File

@ -0,0 +1,56 @@
import _ from 'lodash';
import { FlowNodeModel, Instruction, JOB_STATUS, Processor } from '../..';
export class JSParseInstruction extends Instruction {
async run(node: FlowNodeModel, input: any, processor: Processor) {
const { source = '', JSCode = '', model } = node.config;
const data = processor.getParsedValue(source, node.id);
const query = evalSimulate;
try {
let result = query ? await query(JSCode, { scopes: data, handlers: {}, modules: {} }) : data;
if (typeof result === 'object' && result && model?.length) {
if (Array.isArray(result)) {
result = result.map((item) => mapModel(item, model));
} else {
result = mapModel(result, model);
}
}
return {
result,
status: JOB_STATUS.RESOLVED,
};
} catch (err) {
return {
result: err.toString(),
status: JOB_STATUS.ERROR,
};
}
}
}
function mapModel(data, model) {
if (typeof data !== 'object' || data === null) {
throw new Error('Invalid data: data should be a non-null object');
}
const result = model.reduce((acc, { path, alias }) => {
const key = alias ?? path.replace(/\./g, '_');
const value = _.get(data, path);
acc[key] = value;
return acc;
}, {});
return result;
}
async function evalSimulate(jsCode, { scopes, handlers, modules }) {
try {
return new Function('$root', `with($root) { ${jsCode}; }`)({ scopes, handlers, modules });
} catch (err) {
console.log('err', err);
}
}

View File

@ -0,0 +1 @@
export { default } from './plugin';

View File

@ -0,0 +1,14 @@
import { Plugin } from '@tachybase/server';
import WorkflowPlugin from '../..';
import { NAMESPACE_INSTRUCTION_JS_PARSE } from '../../../common/constants';
import { JSParseInstruction } from './JSParse.instruction';
export class PluginWorkflowJSParseServer extends Plugin {
async load() {
const pluginWorkflow: any = this.app.pm.get(WorkflowPlugin);
pluginWorkflow.registerInstruction(NAMESPACE_INSTRUCTION_JS_PARSE, JSParseInstruction);
}
}
export default PluginWorkflowJSParseServer;