feat: now workflow can response (#1186)

Co-authored-by: sealday <sealday@gmail.com>
Reviewed-on: daoyoucloud/tachybase#1186
This commit is contained in:
sealday 2024-06-14 00:56:39 +08:00
parent 96c9f954e7
commit 5acee2f57f
16 changed files with 596 additions and 37 deletions

View File

@ -336,7 +336,7 @@ export function RemoveButton(
);
}
function WorkflowSelect({ actionType, direct = false, ...props }) {
function WorkflowSelect({ formAction, buttonAction, actionType, direct = false, ...props }) {
const { t } = useTranslation();
const index = ArrayTable.useIndex();
const { setValuesIn } = useForm();
@ -377,17 +377,28 @@ function WorkflowSelect({ actionType, direct = false, ...props }) {
});
const optionFilter = useCallback(
({ type, config }) => {
({ key, type, config }) => {
if (key === props.value) {
return true;
}
const trigger = workflowPlugin.triggers.get(type);
if (trigger.isActionTriggerable === true) {
return true;
}
if (typeof trigger.isActionTriggerable === 'function') {
return trigger.isActionTriggerable(config, { action: actionType, direct });
return trigger.isActionTriggerable(config, {
action: actionType,
formAction,
buttonAction,
/**
* @deprecated
*/
direct: buttonAction === 'customize:triggerWorkflows',
});
}
return false;
},
[workflowPlugin.triggers, actionType, direct],
[props.value, workflowPlugin.triggers, actionType, formAction, buttonAction],
);
return (
@ -436,6 +447,8 @@ export function WorkflowConfig() {
// TODO(refactor): should refactor for getting certain action type, better from 'x-action'.
const formBlock = useFormBlockContext();
const actionType = formBlock?.type || fieldSchema['x-action'];
const formAction = formBlock?.type;
const buttonAction = fieldSchema['x-action'];
const description = {
submit: t('Workflow will be triggered before or after submitting succeeded based on workflow type.', {
@ -510,7 +523,7 @@ export function WorkflowConfig() {
value: '',
},
allowClear: false,
loadData: actionType === 'destroy' ? null : undefined,
loadData: buttonAction === 'destroy' ? null : undefined,
},
default: '',
},
@ -530,6 +543,8 @@ export function WorkflowConfig() {
'x-component-props': {
placeholder: t('Select workflow', { ns: 'workflow' }),
actionType,
formAction,
buttonAction,
direct: fieldSchema['x-action'] === 'customize:triggerWorkflows',
},
required: true,

View File

@ -3,20 +3,33 @@ import { ToposortOptions } from '@tachybase/utils';
import { DataSource } from './data-source';
import { DataSourceFactory } from './data-source-factory';
type DataSourceHook = (dataSource: DataSource) => void;
export class DataSourceManager {
dataSources: Map<string, DataSource>;
/**
* @internal
*/
factory: DataSourceFactory = new DataSourceFactory();
protected middlewares = [];
private onceHooks: Array<DataSourceHook> = [];
constructor(public options = {}) {
this.dataSources = new Map();
this.middlewares = [];
}
get(dataSourceKey: string) {
return this.dataSources.get(dataSourceKey);
}
async add(dataSource: DataSource, options: any = {}) {
await dataSource.load(options);
this.dataSources.set(dataSource.name, dataSource);
for (const hook of this.onceHooks) {
hook(dataSource);
}
}
use(fn: any, options?: ToposortOptions) {
@ -25,17 +38,39 @@ export class DataSourceManager {
middleware() {
return async (ctx, next) => {
const name = ctx.get('x-data-source');
if (name) {
if (this.dataSources.has(name)) {
const ds = this.dataSources.get(name);
ctx.dataSource = ds;
return ds.middleware(this.middlewares)(ctx, next);
} else {
const name = ctx.get('x-data-source') || 'main';
if (!this.dataSources.has(name)) {
ctx.throw(`data source ${name} does not exist`);
}
}
await next();
const ds = this.dataSources.get(name);
ctx.dataSource = ds;
return ds.middleware(this.middlewares)(ctx, next);
};
}
registerDataSourceType(type: string, DataSourceClass: typeof DataSource) {
this.factory.register(type, DataSourceClass);
}
getDataSourceType(type: string): typeof DataSource | undefined {
return this.factory.getClass(type);
}
buildDataSourceByType(type: string, options: any = {}): DataSource {
return this.factory.create(type, options);
}
afterAddDataSource(hook: DataSourceHook) {
this.addHookAndRun(hook);
}
private addHookAndRun(hook: DataSourceHook) {
this.onceHooks.push(hook);
for (const dataSource of this.dataSources.values()) {
hook(dataSource);
}
}
}

View File

@ -54,6 +54,7 @@ export type MergeOptions = {
};
export interface ICollectionManager {
db: any;
registerFieldTypes(types: Record<string, any>): void;
registerFieldInterfaces(interfaces: Record<string, any>): void;
registerCollectionTemplates(templates: Record<string, any>): void;

View File

@ -13,8 +13,10 @@ import PluginWorkflowJSParseClient from './features/js-parse';
import PluginWorkflowJsonParseClient from './features/json-parse';
import { PluginLoop } from './features/loop';
import { PluginManual } from './features/manual';
import { PluginOmniTrigger } from './features/omni-trigger';
import { PluginParallel } from './features/parallel';
import { PluginRequest } from './features/request';
import { PluginResponse } from './features/response';
import { PluginSql } from './features/sql';
import { PluginVariables } from './features/variables';
import { NAMESPACE } from './locale';
@ -85,6 +87,8 @@ export class PluginWorkflow extends Plugin {
await this.pm.add(PluginAPIRegularClient);
await this.pm.add(PluginWorkflowInterceptor);
await this.pm.add(PluginVariables);
await this.pm.add(PluginResponse);
await this.pm.add(PluginOmniTrigger);
}
async load() {
@ -131,16 +135,3 @@ export class PluginWorkflow extends Plugin {
});
}
}
export * from './Branch';
export * from './FlowContext';
export * from './constants';
export * from './nodes';
export { Trigger, useTrigger } from './triggers';
export * from './variable';
export * from './components';
export * from './utils';
export * from './hooks';
export { default as useStyles } from './style';
export * from './variable';
export * from './ExecutionContextProvider';

View File

@ -33,12 +33,9 @@ const recordTriggerWorkflowActionInitializer: SchemaInitializerItemType = {
schema: {
title: '{{t("Submit to workflow", { ns: "workflow" })}}',
'x-component': 'Action',
'x-component-props': {
useProps: '{{ useRecordTriggerWorkflowsActionProps }}',
},
'x-use-component-props': 'useRecordTriggerWorkflowsActionProps',
'x-designer': 'Action.Designer',
'x-action-settings': {
// assignedValues: {},
onSuccess: {
manualClose: true,
redirecting: false,

View File

@ -95,9 +95,9 @@ export class WorkflowTriggerInterceptor extends Trigger {
RadioWithTooltip,
CheckboxGroupWithTooltip,
};
isActionTriggerable = (a, u) => {
const { global: m } = a;
return !m && !u.direct;
isActionTriggerable = (config, context) => {
const { global } = config;
return !global && !context.direct;
};
useVariables(config, options) {

View File

@ -0,0 +1,297 @@
import {
Plugin,
useActionContext,
useBlockRequestContext,
useCollectionDataSource,
useCollectionManager_deprecated,
useCollectValuesToSubmit,
useCompile,
useFilterByTk,
} from '@tachybase/client';
import { useField, useFieldSchema, useForm } from '@tachybase/schema';
import { isURL } from '@tachybase/utils/client';
import { App, message } from 'antd';
import { useNavigate } from 'react-router-dom';
import { CollectionBlockInitializer } from '../../components';
import { lang, tval } from '../../locale';
import { PluginWorkflow } from '../../Plugin';
import { Trigger } from '../../triggers';
import { getCollectionFieldOptions, UseVariableOptions } from '../../variable';
class OmniAction extends Trigger {
title = tval('Custom action event');
description = tval(
`When the "Trigger Workflow" button is clicked, the event is triggered based on the single piece of data where the button is located. For complex data processing that cannot be handled simply by NocoBase\\'s built-in operations (CRUD), you can define a series of operations through a workflow and trigger it with the "Trigger Workflow" button.`,
);
fieldset = {
collection: {
type: 'string',
title: tval('Collection'),
required: true,
'x-decorator': 'FormItem',
'x-component': 'DataSourceCollectionCascader',
'x-reactions': [{ target: 'changed', effects: ['onFieldValueChange'], fulfill: { state: { value: [] } } }],
},
appends: {
type: 'array',
title: tval('Associations to use'),
description: tval(
'Please select the associated fields that need to be accessed in subsequent nodes. With more than two levels of to-many associations may cause performance issue, please use with caution.',
),
'x-decorator': 'FormItem',
'x-component': 'AppendsTreeSelect',
'x-component-props': {
multiple: true,
useCollection() {
const form = useForm();
return form.values?.collection;
},
},
'x-reactions': [{ dependencies: ['collection'], fulfill: { state: { visible: '{{!!$deps[0]}}' } } }],
},
};
scope = { useCollectionDataSource };
components = {};
isActionTriggerable = (config, context) => {
return context.buttonAction === 'customize:triggerWorkflows';
};
useVariables(config: Record<string, any>, options?: UseVariableOptions) {
// eslint-disable-next-line react-hooks/rules-of-hooks
const compile = useCompile();
// eslint-disable-next-line react-hooks/rules-of-hooks
const { getCollectionFields: getCollectionFields } = useCollectionManager_deprecated();
const m = getCollectionFieldOptions({
appends: ['user'],
...options,
fields: [
{
collectionName: 'users',
name: 'user',
type: 'hasOne',
target: 'users',
uiSchema: { title: lang('User acted') },
},
],
compile,
getCollectionFields,
});
return [
...getCollectionFieldOptions({
appends: ['data', ...(config.appends?.map((append) => `data.${append}`) || [])],
fields: [
{
collectionName: config.collection,
name: 'data',
type: 'hasOne',
target: config.collection,
uiSchema: { title: lang('Trigger data') },
},
],
compile,
getCollectionFields,
}),
...m,
{ label: lang('Role of user acted'), value: 'roleName' },
];
}
useInitializers(config) {
return config.collection
? {
name: 'triggerData',
type: 'item',
key: 'triggerData',
title: tval('Trigger data'),
Component: CollectionBlockInitializer,
collection: config.collection,
dataPath: '$context.data',
}
: null;
}
}
function useFormWorkflowCustomActionProps() {
const form = useForm();
const { field, __parent, resource } = useBlockRequestContext();
const { setVisible } = useActionContext();
const filterByTk = useFilterByTk();
const navigate = useNavigate();
const fieldSchema = useFieldSchema();
const _field = useField();
const compile = useCompile();
const { modal } = App.useApp();
const collectionValuesToSubmit = useCollectValuesToSubmit();
return (
_field.componentProps.filterKeys,
{
async onClick() {
const { onSuccess, skipValidator, triggerWorkflows } = fieldSchema?.['x-action-settings'] || {};
if (!skipValidator) {
await form.submit();
}
const values = await collectionValuesToSubmit();
(_field.data = field.data || {}), (_field.data.loading = true);
try {
const B = await resource.trigger({
values,
filterByTk,
triggerWorkflows: triggerWorkflows?.length
? triggerWorkflows
.map((workflow) => [workflow.workflowKey, workflow.context].filter(Boolean).join('!'))
.join(',')
: void 0,
});
if (
((_field.data.data = B),
__parent?.service?.refresh?.(),
setVisible == null || setVisible(false),
!(onSuccess != null && onSuccess.successMessage))
)
return;
onSuccess != null && onSuccess.manualClose
? modal.success({
title: compile(onSuccess?.successMessage),
onOk: async () => {
await form.reset();
return (
onSuccess?.redirecting &&
onSuccess?.redirectTo &&
(isURL(onSuccess.redirectTo)
? (window.location.href = onSuccess.redirectTo)
: navigate(onSuccess.redirectTo))
);
},
})
: message.success(compile(onSuccess == null ? void 0 : onSuccess.successMessage));
} catch (B) {
console.error(B);
} finally {
_field.data.loading = false;
}
},
}
);
}
function useRecordWorkflowCustomTriggerActionProps() {
const compile = useCompile();
const filterByTk = useFilterByTk();
const _field = useField();
const fieldSchema = useFieldSchema();
const { field, resource } = useBlockRequestContext();
const { setVisible, setSubmitted } = useActionContext() as any;
const { modal } = App.useApp();
const navigate = useNavigate();
const { onSuccess, triggerWorkflows } = fieldSchema?.['x-action-settings'] || {};
return {
async onClick(N, w) {
(_field.data = field.data || {}), (_field.data.loading = true);
try {
if (
(await resource.trigger({
filterByTk: filterByTk,
triggerWorkflows: triggerWorkflows?.length
? triggerWorkflows
.map((workflow) => [workflow.workflowKey, workflow.context].filter(Boolean).join('!'))
.join(',')
: void 0,
}),
w && w(),
setVisible == null || setVisible(false),
setSubmitted == null || setSubmitted(true),
!onSuccess?.successMessage)
)
return;
if (onSuccess?.manualClose) {
modal.success({
title: compile(onSuccess?.successMessage),
onOk() {
onSuccess?.redirecting &&
onSuccess?.redirectTo &&
(isURL(onSuccess.redirectTo)
? (window.location.href = onSuccess.redirectTo)
: navigate(onSuccess.redirectTo));
},
});
} else message.success(compile(onSuccess?.successMessage));
} catch (error) {
console.error(error);
} finally {
_field.data.loading = false;
}
},
};
}
const triggerWorkflowItem = {
name: 'triggerWorkflow',
title: tval('Trigger workflow'),
Component: 'CustomizeActionInitializer',
schema: {
title: tval('Trigger workflow'),
'x-component': 'Action',
'x-use-component-props': 'useFormWorkflowCustomActionProps',
'x-designer': 'Action.Designer',
'x-action-settings': {
skipValidator: false,
onSuccess: { manualClose: true, redirecting: false, successMessage: tval('Submitted successfully') },
triggerWorkflows: [],
},
'x-action': 'customize:triggerWorkflows',
},
};
const triggerWorkflowAction = {
name: 'triggerWorkflow',
title: tval('Trigger workflow'),
Component: 'CustomizeActionInitializer',
schema: {
title: tval('Trigger workflow'),
'x-component': 'Action',
'x-use-component-props': 'useRecordWorkflowCustomTriggerActionProps',
'x-designer': 'Action.Designer',
'x-action-settings': {
onSuccess: {
manualClose: true,
redirecting: false,
successMessage: tval('Submitted successfully'),
},
triggerWorkflows: [],
},
'x-action': 'customize:triggerWorkflows',
},
};
const triggerWorkflowLinkItem = {
...triggerWorkflowAction,
schema: { ...triggerWorkflowAction.schema, 'x-component': 'Action.Link' },
};
export class PluginOmniTrigger extends Plugin {
async load() {
this.app.pm.get<PluginWorkflow>('workflow').registerTrigger('custom-action', OmniAction);
this.app.addScopes({
useFormWorkflowCustomActionProps,
useRecordWorkflowCustomTriggerActionProps,
});
this.app.schemaInitializerManager
.get('FormActionInitializers')
.add('customize.triggerWorkflow', triggerWorkflowItem);
this.app.schemaInitializerManager
.get('createForm:configureActions')
.add('customize.triggerWorkflow', triggerWorkflowItem);
this.app.schemaInitializerManager
.get('editForm:configureActions')
.add('customize.triggerWorkflow', triggerWorkflowItem);
this.app.schemaInitializerManager
.get('detailsWithPaging:configureActions')
.add('customize.triggerWorkflow', triggerWorkflowAction);
this.app.schemaInitializerManager
.get('details:configureActions')
.add('customize.triggerWorkflow', triggerWorkflowAction);
this.app.schemaInitializerManager
.get('table:configureItemActions')
.add('customize.triggerWorkflow', triggerWorkflowLinkItem);
this.app.schemaInitializerManager
.get('gridCard:configureItemActions')
.add('customize.triggerWorkflow', triggerWorkflowLinkItem);
this.app.schemaInitializerManager
.get('list:configureItemActions')
.add('customize.triggerWorkflow', triggerWorkflowLinkItem);
}
}

View File

@ -0,0 +1,37 @@
import { Plugin } from '@tachybase/client';
import { RadioWithTooltip } from '../../components';
import { tval } from '../../locale';
import { Instruction } from '../../nodes';
import { PluginWorkflow } from '../../Plugin';
import { WorkflowVariableInput, WorkflowVariableTextArea } from '../../variable';
class ResponseInstruction extends Instruction {
title = tval('Response message');
type = 'response-message';
group = 'extended';
description = tval('Add response message, will be send to client when process of request ends.');
fieldset = {
message: {
type: 'string',
title: tval('Message content'),
description: tval('Supports variables in template.', { name: '{{name}}' }),
'x-decorator': 'FormItem',
'x-component': 'WorkflowVariableTextArea',
},
};
scope = {};
components = {
RadioWithTooltip,
WorkflowVariableTextArea,
WorkflowVariableInput,
};
isAvailable({ workflow }) {
return workflow.type === 'request-interception' || ('action' === workflow.type && workflow.sync);
}
}
export class PluginResponse extends Plugin {
async load() {
this.app.pm.get<PluginWorkflow>('workflow').registerInstruction('response-message', ResponseInstruction);
}
}

View File

@ -11,5 +11,5 @@ export function useWorkflowTranslation() {
return useTranslation(NAMESPACE);
}
export const tval = (key: string) => nTval(key, { ns: NAMESPACE });
export const tval = (key: string, opts={}) => nTval(key, { ns: NAMESPACE, ...opts });

View File

@ -18,8 +18,10 @@ 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';
import { PluginOmniTrigger } from './features/omni-trigger';
import { PluginParallel } from './features/parallel/Plugin';
import { PluginRequest } from './features/request/Plugin';
import { PluginResponse } from './features/response';
import { PluginSql } from './features/sql/Plugin';
import { PluginVariables } from './features/variables';
import initFunctions, { CustomFunction } from './functions';
@ -72,6 +74,8 @@ export default class PluginWorkflowServer extends Plugin {
pluginAPIRegular: PluginWorkflowAPIRegularServer;
pluginInterception: PluginInterception;
pluginVariables: PluginVariables;
pluginResponse: PluginResponse;
pluginOmni: PluginOmniTrigger;
constructor(app: Application, options?: PluginOptions) {
super(app, options);
@ -89,6 +93,8 @@ export default class PluginWorkflowServer extends Plugin {
this.pluginAPIRegular = new PluginWorkflowAPIRegularServer(app, options);
this.pluginInterception = new PluginInterception(app, options);
this.pluginVariables = new PluginVariables(app, options);
this.pluginResponse = new PluginResponse(app, options);
this.pluginOmni = new PluginOmniTrigger(app, options);
}
getLogger(workflowId: ID): Logger {
@ -317,6 +323,8 @@ export default class PluginWorkflowServer extends Plugin {
await this.pluginAPIRegular.load();
await this.pluginInterception.load();
await this.pluginVariables.load();
await this.pluginResponse.load();
await this.pluginOmni.load();
}
toggle(workflow: WorkflowModel, enable?: boolean) {

View File

@ -0,0 +1,132 @@
import { joinCollectionName, parseCollectionName } from '@tachybase/data-source-manager';
import PluginErrorHandler from '@tachybase/plugin-error-handler';
import _ from 'lodash';
import { EXECUTION_STATUS } from '../../constants';
import Trigger from '../../triggers';
import { toJSON } from '../../utils';
class CustomActionInterceptionError extends Error {
status = 400;
messages = [];
constructor(message) {
super(message);
this.name = 'CustomActionInterceptionError';
}
}
export class OmniTrigger extends Trigger {
static TYPE = 'custom-action';
triggerAction = async (context, next) => {
const {
resourceName,
actionName,
params: { filterByTk, values, triggerWorkflows = '' },
} = context.action;
if (actionName !== 'trigger' || resourceName === 'workflows') {
return next();
}
const { currentUser, currentRole } = context.state;
const { model: UserModel } = this.workflow.db.getCollection('users');
const userInfo = {
user: UserModel.build(currentUser).desensitize(),
roleName: currentRole,
};
const dataSourceHeader = context.get('x-data-source');
const jointCollectionName = joinCollectionName(dataSourceHeader, resourceName);
const triggerWorkflowsMap = new Map();
const triggerWorkflowsArray = [];
for (const trigger of triggerWorkflows.split(',')) {
const [key, path] = trigger.split('!');
triggerWorkflowsMap.set(key, path);
triggerWorkflowsArray.push(key);
}
const workflows = Array.from(this.workflow.enabledCache.values())
.filter((item) => item.type === OmniTrigger.TYPE && item.config.collection === jointCollectionName)
.sort((a, b) => {
const aIndex = triggerWorkflowsArray.indexOf(a.key);
const bIndex = triggerWorkflowsArray.indexOf(b.key);
if (aIndex === -1 && bIndex === -1) {
return a.id - b.id;
}
if (aIndex === -1) {
return 1;
}
if (bIndex === -1) {
return -1;
}
return aIndex - bIndex;
});
const syncGroup = [];
const asyncGroup = [];
for (const workflow of workflows) {
const { appends = [] } = workflow.config;
const [dataSourceName, collectionName] = parseCollectionName(workflow.config.collection);
const dataPath = triggerWorkflowsMap.get(workflow.key);
const event = [workflow];
const { repository } = context.app.dataSourceManager.dataSources
.get(dataSourceName)
.collectionManager.getCollection(collectionName);
const formData = dataPath ? _.get(values, dataPath) : values;
let data = formData;
if (filterByTk != null) {
data = await repository.findOne({ filterByTk, appends });
if (!data) {
continue;
}
Object.assign(data, formData);
}
// @ts-ignore
event.push({ data: toJSON(data), ...userInfo });
(workflow.sync ? syncGroup : asyncGroup).push(event);
}
for (const event of syncGroup) {
const processor = await this.workflow.trigger(event[0], event[1], { httpContext: context });
if (!processor) {
return context.throw(500);
}
const { lastSavedJob, nodesMap } = processor;
const lastNode = nodesMap.get(lastSavedJob?.nodeId);
if (processor.execution.status === EXECUTION_STATUS.RESOLVED) {
if (lastNode?.type === 'end') {
return;
}
continue;
}
if (processor.execution.status < EXECUTION_STATUS.STARTED) {
if (lastNode?.type !== 'end') {
return context.throw(500, 'Workflow on your action failed, please contact the administrator');
}
const err = new CustomActionInterceptionError('Request is intercepted by workflow');
err.status = 400;
err.messages = context.state.messages;
return context.throw(err.status, err);
}
return context.throw(500, 'Workflow on your action hangs, please contact the administrator');
}
for (const event of asyncGroup) {
this.workflow.trigger(event[0], event[1]);
}
await next();
};
constructor(workflow) {
super(workflow);
this.workflow.app.dataSourceManager.afterAddDataSource((dataSource) => {
if (!dataSource.collectionManager?.db) {
return;
}
dataSource.resourceManager.registerActionHandler('trigger', this.triggerAction);
});
workflow.app.pm.get(PluginErrorHandler).errorHandler.register(
(err) => err instanceof CustomActionInterceptionError,
async (err, ctx) => {
ctx.body = {
errors: err.messages,
};
ctx.status = err.status;
},
);
}
on() {}
off() {}
}

View File

@ -0,0 +1,11 @@
import { Plugin } from '@tachybase/server';
import PluginWorkflowServer from '../../Plugin';
import { OmniTrigger } from './CustomActionTrigger';
export class PluginOmniTrigger extends Plugin {
async load() {
const workflowPlugin = this.app.pm.get(PluginWorkflowServer) as PluginWorkflowServer;
workflowPlugin.registerTrigger('custom-action', OmniTrigger);
}
}

View File

@ -0,0 +1 @@
export * from './Plugin';

View File

@ -0,0 +1,11 @@
import { Plugin } from '@tachybase/server';
import PluginWorkflowServer from '../../Plugin';
import { ResponseInstruction } from './ResponseMessageInstruction';
export class PluginResponse extends Plugin {
async load() {
const workflowPlugin = this.app.pm.get(PluginWorkflowServer) as PluginWorkflowServer;
workflowPlugin.registerInstruction('response-message', ResponseInstruction);
}
}

View File

@ -0,0 +1,22 @@
import { JOB_STATUS } from '../../constants';
import Instruction from '../../instructions';
export class ResponseInstruction extends Instruction {
async run(node, prevJob, processor) {
const { httpContext } = processor.options;
if (!httpContext.state) {
httpContext.state = {};
}
if (!httpContext.state.messages) {
httpContext.state.messages = [];
}
const message = processor.getParsedValue(node.config.message, node.id);
if (message) {
httpContext.state.messages.push({ message });
}
return {
status: JOB_STATUS.RESOLVED,
result: message,
};
}
}

View File

@ -0,0 +1 @@
export * from './Plugin';