feat: plugin-workflow-json-parse (#1091)

Reviewed-on: daoyoucloud/tachybase#1091
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 18:26:25 +08:00 committed by sealday
parent 1ff8bc8157
commit a947d639fc
11 changed files with 264 additions and 39 deletions

View File

@ -12,6 +12,9 @@
"./client": "./dist/client/index.js" "./client": "./dist/client/index.js"
}, },
"main": "./dist/server/index.js", "main": "./dist/server/index.js",
"dependencies": {
"jsonata": "^2.0.5"
},
"devDependencies": { "devDependencies": {
"@ant-design/icons": "~5.3.6", "@ant-design/icons": "~5.3.6",
"@tachybase/components": "workspace:*", "@tachybase/components": "workspace:*",

View File

@ -0,0 +1,128 @@
import { css } from '@tachybase/client';
import { ArrayTable } from '@tachybase/components';
import { Instruction, WorkflowVariableInput } from '@tachybase/plugin-workflow/client';
import { NAMESPACE_INSTRUCTION_JSON_PARSE } from '../../../common/constants';
import { tval } from '../../locale';
export class JSONParseInstruction extends Instruction {
title = tval('JSON Parse');
type = NAMESPACE_INSTRUCTION_JSON_PARSE;
// XXX: 这里应该定义在 workflow 里的一个统一的地方. workflow 本身没处理好这块, 先这样直接写了.
group = 'extended';
description = tval('Get specific data from JSON result of any node BY jsonata; https://jsonata.org/');
fieldset = {
source: {
type: 'string',
title: tval('Data source'),
'x-decorator': 'FormItem',
'x-component': 'WorkflowVariableInput',
'x-component-props': {
changeOnSelect: true,
},
required: true,
},
expression: {
type: 'string',
title: tval('Query expression'),
'x-decorator': 'FormItem',
'x-component': 'Input',
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.',
),
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,
};
}

View File

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

View File

@ -1,34 +1,34 @@
import React from 'react'; import React from 'react';
import { Plugin } from '@tachybase/client'; import { Plugin } from '@tachybase/client';
import { Registry } from '@tachybase/utils/client'; import { Registry } from '@tachybase/utils/client';
import { ExecutionPage } from './ExecutionPage'; import { ExecutionPage } from './ExecutionPage';
import { WorkflowPage } from './WorkflowPage'; import { PluginActionTrigger } from './features/action-trigger';
import { WorkflowPane } from './WorkflowPane'; import { PluginAggregate } from './features/aggregate';
import { Trigger } from './triggers'; import { PluginDelay } from './features/delay';
import CollectionTrigger from './triggers/collection'; import { PluginDaynamicCalculation } from './features/dynamic-calculation';
import ScheduleTrigger from './triggers/schedule'; import PluginWorkflowJsonParseClient from './features/json-parse';
import { PluginLoop } from './features/loop';
import { PluginManual } from './features/manual';
import { PluginParallel } from './features/parallel';
import { PluginRequest } from './features/request';
import { PluginSql } from './features/sql';
import { NAMESPACE } from './locale';
import { Instruction } from './nodes'; import { Instruction } from './nodes';
import CalculationInstruction from './nodes/calculation'; import CalculationInstruction from './nodes/calculation';
import ConditionInstruction from './nodes/condition'; import ConditionInstruction from './nodes/condition';
import CreateInstruction from './nodes/create';
import DestroyInstruction from './nodes/destroy';
import EndInstruction from './nodes/end'; import EndInstruction from './nodes/end';
import QueryInstruction from './nodes/query'; import QueryInstruction from './nodes/query';
import CreateInstruction from './nodes/create';
import UpdateInstruction from './nodes/update'; import UpdateInstruction from './nodes/update';
import DestroyInstruction from './nodes/destroy';
import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './utils';
import { NAMESPACE } from './locale';
import { customizeSubmitToWorkflowActionSettings } from './settings/customizeSubmitToWorkflowActionSettings'; import { customizeSubmitToWorkflowActionSettings } from './settings/customizeSubmitToWorkflowActionSettings';
import { PluginSql } from './features/sql'; import { Trigger } from './triggers';
import { PluginRequest } from './features/request'; import CollectionTrigger from './triggers/collection';
import { PluginParallel } from './features/parallel'; import ScheduleTrigger from './triggers/schedule';
import { PluginLoop } from './features/loop'; import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './utils';
import { PluginDaynamicCalculation } from './features/dynamic-calculation'; import { WorkflowPage } from './WorkflowPage';
import { PluginDelay } from './features/delay'; import { WorkflowPane } from './WorkflowPane';
import { PluginAggregate } from './features/aggregate';
import { PluginActionTrigger } from './features/action-trigger';
import { PluginManual } from './features/manual';
export default class PluginWorkflowClient extends Plugin { export default class PluginWorkflowClient extends Plugin {
triggers = new Registry<Trigger>(); triggers = new Registry<Trigger>();
@ -76,6 +76,7 @@ export default class PluginWorkflowClient extends Plugin {
await this.pm.add(PluginDelay); await this.pm.add(PluginDelay);
await this.pm.add(PluginAggregate); await this.pm.add(PluginAggregate);
await this.pm.add(PluginActionTrigger); await this.pm.add(PluginActionTrigger);
await this.pm.add(PluginWorkflowJsonParseClient);
} }
async load() { async load() {

View File

@ -1,4 +1,4 @@
import { i18n } from '@tachybase/client'; import { i18n, tval as nTval } from '@tachybase/client';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export const NAMESPACE = 'workflow'; export const NAMESPACE = 'workflow';
@ -10,3 +10,6 @@ export function lang(key: string, options = {}) {
export function useWorkflowTranslation() { export function useWorkflowTranslation() {
return useTranslation(NAMESPACE); return useTranslation(NAMESPACE);
} }
export const tval = (key: string) => nTval(key, { ns: NAMESPACE });

View File

@ -0,0 +1 @@
export const NAMESPACE_INSTRUCTION_JSON_PARSE = 'json-parse';

View File

@ -1,38 +1,37 @@
import path from 'path'; import path from 'path';
import LRUCache from 'lru-cache';
import { Op, Transactionable } from '@tachybase/database'; import { Op, Transactionable } from '@tachybase/database';
import { Logger, LoggerOptions } from '@tachybase/logger';
import Application, { Plugin, PluginOptions } from '@tachybase/server'; import Application, { Plugin, PluginOptions } from '@tachybase/server';
import { Registry } from '@tachybase/utils'; import { Registry } from '@tachybase/utils';
import { Logger, LoggerOptions } from '@tachybase/logger'; import LRUCache from 'lru-cache';
import Processor from './Processor';
import initActions from './actions'; import initActions from './actions';
import { EXECUTION_STATUS } from './constants'; import { EXECUTION_STATUS } from './constants';
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 PluginWorkflowJSONParseServer from './features/json-parse/plugin';
import { PluginLoop } from './features/loop/Plugin';
import { PluginManual } from './features/manual/Plugin';
import { PluginParallel } from './features/parallel/Plugin';
import { PluginRequest } from './features/request/Plugin';
import { PluginSql } from './features/sql/Plugin';
import initFunctions, { CustomFunction } from './functions'; import initFunctions, { CustomFunction } from './functions';
import Trigger from './triggers';
import CollectionTrigger from './triggers/CollectionTrigger';
import ScheduleTrigger from './triggers/ScheduleTrigger';
import { Instruction, InstructionInterface } from './instructions'; import { Instruction, InstructionInterface } from './instructions';
import CalculationInstruction from './instructions/CalculationInstruction'; import CalculationInstruction from './instructions/CalculationInstruction';
import ConditionInstruction from './instructions/ConditionInstruction'; import ConditionInstruction from './instructions/ConditionInstruction';
import EndInstruction from './instructions/EndInstruction';
import CreateInstruction from './instructions/CreateInstruction'; import CreateInstruction from './instructions/CreateInstruction';
import DestroyInstruction from './instructions/DestroyInstruction'; import DestroyInstruction from './instructions/DestroyInstruction';
import EndInstruction from './instructions/EndInstruction';
import QueryInstruction from './instructions/QueryInstruction'; import QueryInstruction from './instructions/QueryInstruction';
import UpdateInstruction from './instructions/UpdateInstruction'; import UpdateInstruction from './instructions/UpdateInstruction';
import Processor from './Processor';
import Trigger from './triggers';
import CollectionTrigger from './triggers/CollectionTrigger';
import ScheduleTrigger from './triggers/ScheduleTrigger';
import type { ExecutionModel, JobModel, WorkflowModel } from './types'; import type { ExecutionModel, JobModel, WorkflowModel } from './types';
import { PluginSql } from './features/sql/Plugin';
import { PluginRequest } from './features/request/Plugin';
import { PluginParallel } from './features/parallel/Plugin';
import { PluginManual } from './features/manual/Plugin';
import { PluginLoop } from './features/loop/Plugin';
import { PluginDynamicCalculation } from './features/dynamic-calculation/Plugin';
import { PluginDelay } from './features/delay/Plugin';
import { PluginAggregate } from './features/aggregate/Plugin';
import { PluginActionTrigger } from './features/action-trigger/Plugin';
type ID = number | string; type ID = number | string;
@ -64,6 +63,7 @@ export default class PluginWorkflowServer extends Plugin {
pluginDelay: PluginDelay; pluginDelay: PluginDelay;
pluginAggregate: PluginAggregate; pluginAggregate: PluginAggregate;
pluginActionTrigger: PluginActionTrigger; pluginActionTrigger: PluginActionTrigger;
pluginJSONParse: PluginWorkflowJSONParseServer;
constructor(app: Application, options?: PluginOptions) { constructor(app: Application, options?: PluginOptions) {
super(app, options); super(app, options);
@ -76,6 +76,7 @@ export default class PluginWorkflowServer extends Plugin {
this.pluginDelay = new PluginDelay(app, options); this.pluginDelay = new PluginDelay(app, options);
this.pluginAggregate = new PluginAggregate(app, options); this.pluginAggregate = new PluginAggregate(app, options);
this.pluginActionTrigger = new PluginActionTrigger(app, options); this.pluginActionTrigger = new PluginActionTrigger(app, options);
this.pluginJSONParse = new PluginWorkflowJSONParseServer(app, options);
} }
getLogger(workflowId: ID): Logger { getLogger(workflowId: ID): Logger {
@ -299,6 +300,7 @@ export default class PluginWorkflowServer extends Plugin {
await this.pluginManual.load(); await this.pluginManual.load();
await this.pluginParallel.load(); await this.pluginParallel.load();
await this.pluginRequest.load(); await this.pluginRequest.load();
await this.pluginJSONParse.load();
} }
toggle(workflow: WorkflowModel, enable?: boolean) { toggle(workflow: WorkflowModel, enable?: boolean) {

View File

@ -0,0 +1,50 @@
import jsonata from 'jsonata';
import _ from 'lodash';
import { FlowNodeModel, Instruction, JOB_STATUS, Processor } from '../..';
export class JSONParseInstruction extends Instruction {
engine = (expression, scope) => jsonata(expression).evaluate(scope);
async run(node: FlowNodeModel, input: any, processor: Processor) {
const { source = '', expression = '', model } = node.config;
const data = processor.getParsedValue(source, node.id);
const query = this.engine;
try {
let result = query ? await query(expression, data) : 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;
}

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_JSON_PARSE } from '../../../common/constants';
import { JSONParseInstruction } from './JSONParse.instruction';
export class PluginWorkflowJSONParseServer extends Plugin {
async load() {
const pluginWorkflow: any = this.app.pm.get(WorkflowPlugin);
pluginWorkflow.registerInstruction(NAMESPACE_INSTRUCTION_JSON_PARSE, JSONParseInstruction);
}
}
export default PluginWorkflowJSONParseServer;

View File

@ -3946,6 +3946,9 @@ importers:
'@tachybase/utils': '@tachybase/utils':
specifier: workspace:* specifier: workspace:*
version: link:../../../core/utils version: link:../../../core/utils
jsonata:
specifier: ^2.0.5
version: 2.0.5
devDependencies: devDependencies:
'@ant-design/icons': '@ant-design/icons':
specifier: ~5.3.6 specifier: ~5.3.6
@ -20859,6 +20862,11 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
hasBin: true hasBin: true
/jsonata@2.0.5:
resolution: {integrity: sha512-wEse9+QLIIU5IaCgtJCPsFi/H4F3qcikWzF4bAELZiRz08ohfx3Q6CjDRf4ZPF5P/92RI3KIHtb7u3jqPaHXdQ==}
engines: {node: '>= 8'}
dev: false
/jsonc-parser@3.2.0: /jsonc-parser@3.2.0:
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}