feat(workflow): support Http Request Node (#1102)
* feat(workflow): support Http Request Node * style(workflow): hTTP Request ui title * style(workflow): request node ui title fix * feat(workflow): support timeout config,ignoreFail etc * refactor(workflow): request node Instruction remove unused input.result from templateVars * fix(workflow): fix locale * fix(workflow): perfect request implementation
This commit is contained in:
parent
6f4f601ba8
commit
9b4139e28a
@ -18,9 +18,12 @@
|
||||
"classnames": "^2.3.1",
|
||||
"cron-parser": "4.4.0",
|
||||
"json-templates": "^4.2.0",
|
||||
"react-js-cron": "^1.4.0"
|
||||
"react-js-cron": "^1.4.0",
|
||||
"ejs": "^3.1.8",
|
||||
"react-copy-to-clipboard": "^5.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ejs": "^3.1.1",
|
||||
"@nocobase/test": "0.8.0-alpha.13"
|
||||
},
|
||||
"gitHead": "ce588eefb0bfc50f7d5bbee575e0b5e843bf6644"
|
||||
|
121
packages/plugins/workflow/src/client/components/EjsTextArea.tsx
Normal file
121
packages/plugins/workflow/src/client/components/EjsTextArea.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Row, Col } from 'antd';
|
||||
import { CopyOutlined, ToolOutlined } from '@ant-design/icons';
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
|
||||
import { Operand, VariableTypes, VariableTypesContext } from '../calculators';
|
||||
import { Button, Input, message, Tooltip } from 'antd';
|
||||
import { useFlowContext } from '../FlowContext';
|
||||
import { useCollectionManager, useCompile } from '@nocobase/client';
|
||||
import { useWorkflowTranslation } from '../locale';
|
||||
|
||||
function getVal(operand) {
|
||||
const { options } = operand;
|
||||
const { t } = useWorkflowTranslation();
|
||||
const compile = useCompile();
|
||||
const { getCollectionFields } = useCollectionManager();
|
||||
const { nodes } = useFlowContext();
|
||||
|
||||
switch (operand.type) {
|
||||
case '$context':
|
||||
if (options?.path) {
|
||||
return `<%=ctx.${options.path}%>`;
|
||||
}
|
||||
break;
|
||||
case '$jobsMapByNodeId':
|
||||
if (options?.nodeId) {
|
||||
const node = nodes.find((n) => n.id == options.nodeId);
|
||||
if (node) {
|
||||
if (node.type === 'calculation') {
|
||||
return `<%= node.${options.nodeId} // ${t('Calculation result')} %>`;
|
||||
}
|
||||
if (options.path && node?.config?.collection) {
|
||||
const fields = getCollectionFields(node?.config?.collection);
|
||||
const field = fields.find((f) => f.name == options.path);
|
||||
if (field) {
|
||||
return `<%= node[${options.nodeId}].${options.path} // ${compile(
|
||||
field.uiSchema?.title || field.name,
|
||||
)} %>`;
|
||||
}
|
||||
return `<%= node[${options.nodeId}].${options.path}%>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
const AvailableVariableTool = (props) => {
|
||||
const { t } = useWorkflowTranslation();
|
||||
const [operand, setOperand] = useState({ type: '$context', value: '' });
|
||||
return (
|
||||
<Row style={{ width: '100%' }}>
|
||||
<Col span={10}>
|
||||
<Operand value={operand} onChange={(v) => setOperand(v)} />
|
||||
</Col>
|
||||
<Col span={10}>
|
||||
<Input value={getVal(operand)} disabled />
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<CopyToClipboard
|
||||
text={getVal(operand)}
|
||||
onCopy={() => {
|
||||
message.success({
|
||||
content: t('Copy success!'),
|
||||
duration: 1,
|
||||
style: {
|
||||
marginTop: '20vh',
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Tooltip title={t('Copy variable output template statement')}>
|
||||
<Button icon={<CopyOutlined />} />
|
||||
</Tooltip>
|
||||
</CopyToClipboard>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export default function (props) {
|
||||
const [showTool, setShowTool] = useState(false);
|
||||
const { t } = useWorkflowTranslation();
|
||||
const requestVariableTypes = {
|
||||
$context: VariableTypes.$context,
|
||||
$jobsMapByNodeId: VariableTypes.$jobsMapByNodeId,
|
||||
};
|
||||
return (
|
||||
<VariableTypesContext.Provider value={requestVariableTypes}>
|
||||
<Row style={{ width: '100%' }}>
|
||||
<Col span={24}>
|
||||
<Tooltip title={t('Show available variable tool')}>
|
||||
<Button type={'text'} icon={<ToolOutlined />} onClick={() => setShowTool(!showTool)} />
|
||||
</Tooltip>
|
||||
</Col>
|
||||
</Row>
|
||||
{showTool && <AvailableVariableTool></AvailableVariableTool>}
|
||||
|
||||
<Row style={{ width: '100%' }}>
|
||||
<Col span={24}>
|
||||
<Input.TextArea {...props} />
|
||||
</Col>
|
||||
</Row>
|
||||
{showTool && (
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<div className="ant-formily-item-extra">
|
||||
{props.description}
|
||||
{' '}
|
||||
{t('Syntax see')}{' '}
|
||||
<a target={'_blank'} href={'https://ejs.co'}>
|
||||
ejs
|
||||
</a>
|
||||
{' '}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</VariableTypesContext.Provider>
|
||||
);
|
||||
}
|
@ -85,4 +85,22 @@ export default {
|
||||
"Fields that are not assigned a value will be set to the default value, and those that do not have a default value are set to null.": "Fields that are not assigned a value will be set to the default value, and those that do not have a default value are set to null.",
|
||||
"Trigger in executed workflow cannot be modified": "Trigger in executed workflow cannot be modified",
|
||||
"Node in executed workflow cannot be modified": "Node in executed workflow cannot be modified",
|
||||
|
||||
'HTTP request': 'HTTP request',
|
||||
'URL': 'URL',
|
||||
'You can use the above available variables in URL.': 'You can use the above available variables in URL.',
|
||||
'Request headers': 'Request headers',
|
||||
'Name(e.g. Content-Type)': 'Name(e.g. Content-Type)',
|
||||
'Value(e.g. Application/json)': 'Value(e.g. Application/json)',
|
||||
'Add request header': 'Add request header',
|
||||
'HTTP method': 'HTTP method',
|
||||
'Request data': 'Request data',
|
||||
'Input request data': 'Input request data',
|
||||
'You can use the above available variables in request data.': 'You can use the above available variables in request data.',
|
||||
'Copy success!': 'Copy success!',
|
||||
'Copy variable output template statement': 'Copy variable output template statement',
|
||||
'Default headers is Content-Type: application/json': 'Default headers is Content-Type: application/json',
|
||||
'Ignore fail request and continue workflow': 'Ignore fail request and continue workflow',
|
||||
'Syntax see': 'Syntax see',
|
||||
'Show available variable tool': 'Show available variable tool'
|
||||
};
|
||||
|
@ -125,4 +125,22 @@ export default {
|
||||
'Fields that are not assigned a value will be set to the default value, and those that do not have a default value are set to null.': '未被赋值的字段将被设置为默认值,没有默认值的设置为空值。',
|
||||
'Trigger in executed workflow cannot be modified': '已经执行过工作流的触发器不能被修改',
|
||||
'Node in executed workflow cannot be modified': '已经执行过工作流中的节点不能被修改',
|
||||
|
||||
'HTTP request': 'HTTP 请求',
|
||||
'URL': '地址',
|
||||
'You can use the above available variables in URL.': '您可以在地址中使用使用上面的可用变量。',
|
||||
'Request headers': '请求头',
|
||||
'Name(e.g. Content-Type)': '名称(例如 Content-Type)',
|
||||
'Value(e.g. Application/json)': '值(例如 Application/json)',
|
||||
'Add request header': '添加请求头',
|
||||
'HTTP method': 'HTTP 方法',
|
||||
'Request data': '请求数据',
|
||||
'Input request data': '输入请求数据',
|
||||
'You can use the above available variables in request data.': '您可以在请求数据中使用上面的可用变量。',
|
||||
'Copy success!': '拷贝成功!',
|
||||
'Copy variable output template statement': '拷贝变量输出模版语句',
|
||||
'Default headers is Content-Type: application/json': '默认请求头是 Content-Type: application/json',
|
||||
'Ignore fail request and continue workflow': '忽略失败请求并继续工作流',
|
||||
'Syntax see': '语法参考',
|
||||
'Show available variable tool':'显示可用变量工具'
|
||||
};
|
||||
|
@ -25,6 +25,7 @@ import update from './update';
|
||||
import destroy from './destroy';
|
||||
import { JobStatusOptions, JobStatusOptionsMap } from '../constants';
|
||||
import { lang, NAMESPACE } from '../locale';
|
||||
import request from "./request";
|
||||
|
||||
export interface Instruction {
|
||||
title: string;
|
||||
@ -51,6 +52,7 @@ instructions.register('query', query);
|
||||
instructions.register('create', create);
|
||||
instructions.register('update', update);
|
||||
instructions.register('destroy', destroy);
|
||||
instructions.register('request', request);
|
||||
|
||||
function useUpdateAction() {
|
||||
const form = useForm();
|
||||
|
142
packages/plugins/workflow/src/client/nodes/request.tsx
Normal file
142
packages/plugins/workflow/src/client/nodes/request.tsx
Normal file
@ -0,0 +1,142 @@
|
||||
import { NAMESPACE } from '../locale';
|
||||
import { ArrayItems } from '@formily/antd';
|
||||
import React from 'react';
|
||||
import EjsTextArea from '../components/EjsTextArea';
|
||||
|
||||
|
||||
|
||||
export default {
|
||||
title: `{{t("HTTP request", { ns: "${NAMESPACE}" })}}`,
|
||||
type: 'request',
|
||||
group: 'extended',
|
||||
fieldset: {
|
||||
'config.url': {
|
||||
type: 'string',
|
||||
name: 'config.url',
|
||||
required: true,
|
||||
title: `{{t("URL", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-decorator-props': {},
|
||||
'x-component': 'EjsTextArea',
|
||||
'x-component-props': {
|
||||
autoSize: {
|
||||
minRows: 1,
|
||||
},
|
||||
placeholder: 'https://xxxxxx',
|
||||
description: `{{t("You can use the above available variables in URL.", { ns: "${NAMESPACE}" })}}`,
|
||||
},
|
||||
},
|
||||
'config.timeout': {
|
||||
type: 'number',
|
||||
name: 'config.timeout',
|
||||
required: true,
|
||||
title: `{{t("Timeout config", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-decorator-props': {},
|
||||
'x-component': 'InputNumber',
|
||||
'x-component-props': {
|
||||
addonAfter: `{{t("ms", { ns: "${NAMESPACE}" })}}`,
|
||||
min: 1,
|
||||
step: 1000,
|
||||
defaultValue: 5000,
|
||||
},
|
||||
},
|
||||
'config.headers': {
|
||||
type: 'array',
|
||||
name: 'config.headers',
|
||||
'x-component': 'ArrayItems',
|
||||
'x-decorator': 'FormItem',
|
||||
title: `{{t("Request headers", { ns: "${NAMESPACE}" })}}`,
|
||||
description: `{{t("Default headers is Content-Type: application/json", { ns: "${NAMESPACE}" })}}`,
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
space: {
|
||||
type: 'void',
|
||||
'x-component': 'Space',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {
|
||||
placeholder: `{{t("Name(e.g. Content-Type)", { ns: "${NAMESPACE}" })}}`,
|
||||
},
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {
|
||||
placeholder: `{{t("Value(e.g. Application/json)", { ns: "${NAMESPACE}" })}}`,
|
||||
},
|
||||
},
|
||||
remove: {
|
||||
type: 'void',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'ArrayItems.Remove',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
add: {
|
||||
type: 'void',
|
||||
title: `{{t("Add request header", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-component': 'ArrayItems.Addition',
|
||||
},
|
||||
},
|
||||
},
|
||||
'config.method': {
|
||||
type: 'string',
|
||||
name: 'config.method',
|
||||
required: true,
|
||||
title: `{{t("HTTP method", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
'x-component-props': {
|
||||
defaultValue: 'POST',
|
||||
showSearch: false,
|
||||
allowClear: false,
|
||||
},
|
||||
enum: [
|
||||
{ label: 'GET', value: 'GET' },
|
||||
{ label: 'POST', value: 'POST' },
|
||||
{ label: 'PUT', value: 'PUT' },
|
||||
{ label: 'PATCH', value: 'PATCH' },
|
||||
{ label: 'DELETE', value: 'DELETE' },
|
||||
],
|
||||
},
|
||||
'config.data': {
|
||||
type: 'string',
|
||||
name: 'config.data',
|
||||
'x-hidden': false,
|
||||
title: `{{t("Request data", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-decorator-props': {},
|
||||
'x-component': 'EjsTextArea',
|
||||
'x-component-props': {
|
||||
autoSize: {
|
||||
minRows: 5,
|
||||
},
|
||||
placeholder: `{{t("Input request data", { ns: "${NAMESPACE}" })}}`,
|
||||
description: `{{t("You can use the above available variables in request data.", { ns: "${NAMESPACE}" })}}`,
|
||||
},
|
||||
|
||||
},
|
||||
'config.ignoreFail': {
|
||||
type: 'boolean',
|
||||
name: 'config.ignoreFail',
|
||||
title: `{{t("Ignore fail request and continue workflow", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Checkbox',
|
||||
}
|
||||
},
|
||||
view: {},
|
||||
scope: {},
|
||||
components: {
|
||||
ArrayItems,
|
||||
EjsTextArea,
|
||||
},
|
||||
};
|
@ -0,0 +1,247 @@
|
||||
/**
|
||||
* @jest-environment node
|
||||
*/
|
||||
|
||||
import { Application } from '@nocobase/server';
|
||||
import Database from '@nocobase/database';
|
||||
import { getApp, sleep } from '..';
|
||||
import { RequestConfig } from '../../instructions/request';
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import { JOB_STATUS } from '../../constants';
|
||||
|
||||
const testUrl = 'https://nocobase.com/test';
|
||||
const url_400 = 'https://nocobase.com/400';
|
||||
const timeoutUrl = 'https://nocobase.com/timeout';
|
||||
jest.mock('axios', () => {
|
||||
return {
|
||||
request: async (config: AxiosRequestConfig) => {
|
||||
await sleep(1000);
|
||||
if (config.url === url_400) {
|
||||
return {
|
||||
data: config,
|
||||
status: 400,
|
||||
};
|
||||
}
|
||||
if (config.url === timeoutUrl) {
|
||||
throw new Error('timeout');
|
||||
}
|
||||
return {
|
||||
data: config,
|
||||
status: 200,
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('workflow > instructions > request', () => {
|
||||
let app: Application;
|
||||
let db: Database;
|
||||
let PostRepo;
|
||||
let WorkflowModel;
|
||||
let workflow;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await getApp();
|
||||
|
||||
db = app.db;
|
||||
WorkflowModel = db.getCollection('workflows').model;
|
||||
PostRepo = db.getCollection('posts').repository;
|
||||
|
||||
workflow = await WorkflowModel.create({
|
||||
title: 'test workflow',
|
||||
enabled: true,
|
||||
type: 'collection',
|
||||
config: {
|
||||
mode: 1,
|
||||
collection: 'posts',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => app.stop());
|
||||
|
||||
describe('request', () => {
|
||||
it('request', async () => {
|
||||
await workflow.createNode({
|
||||
type: 'request',
|
||||
config: {
|
||||
url: 'https://www.baidu.com?name=lily&age=20',
|
||||
method: 'GET',
|
||||
} as RequestConfig,
|
||||
});
|
||||
|
||||
await PostRepo.create({ values: { title: 't1' } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
let [execution] = await workflow.getExecutions();
|
||||
let [job] = await execution.getJobs();
|
||||
expect(job.status).toEqual(JOB_STATUS.PENDING);
|
||||
|
||||
await sleep(2000);
|
||||
|
||||
[execution] = await workflow.getExecutions();
|
||||
[job] = await execution.getJobs();
|
||||
expect(job.status).toEqual(JOB_STATUS.RESOLVED);
|
||||
});
|
||||
|
||||
it('request - url with variable template', async () => {
|
||||
await workflow.createNode({
|
||||
type: 'request',
|
||||
config: {
|
||||
url: 'https://www.xxx.com?title=<%= ctx.data.title // 测试 %>',
|
||||
method: 'GET',
|
||||
} as RequestConfig,
|
||||
});
|
||||
|
||||
await PostRepo.create({ values: { title: 't1' } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
let [execution] = await workflow.getExecutions();
|
||||
let [job] = await execution.getJobs();
|
||||
expect(job.status).toEqual(JOB_STATUS.PENDING);
|
||||
|
||||
await sleep(1000);
|
||||
|
||||
[execution] = await workflow.getExecutions();
|
||||
[job] = await execution.getJobs();
|
||||
expect(job.status).toEqual(JOB_STATUS.RESOLVED);
|
||||
expect(job.result.url).toEqual('https://www.xxx.com?title=t1');
|
||||
});
|
||||
|
||||
it('request - timeout', async () => {
|
||||
await workflow.createNode({
|
||||
type: 'request',
|
||||
config: {
|
||||
url: timeoutUrl,
|
||||
method: 'GET',
|
||||
timeout: 1000,
|
||||
} as RequestConfig,
|
||||
});
|
||||
|
||||
await PostRepo.create({ values: { title: 't1' } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
let [execution] = await workflow.getExecutions();
|
||||
let [job] = await execution.getJobs();
|
||||
expect(job.status).toEqual(JOB_STATUS.PENDING);
|
||||
|
||||
await sleep(1000);
|
||||
|
||||
[execution] = await workflow.getExecutions();
|
||||
[job] = await execution.getJobs();
|
||||
expect(job.status).toEqual(JOB_STATUS.REJECTED);
|
||||
expect(job.result).toMatch('timeout');
|
||||
|
||||
});
|
||||
|
||||
it('request - ignoreFail', async () => {
|
||||
await workflow.createNode({
|
||||
type: 'request',
|
||||
config: {
|
||||
url: timeoutUrl,
|
||||
method: 'GET',
|
||||
timeout: 1000,
|
||||
ignoreFail: true,
|
||||
} as RequestConfig,
|
||||
});
|
||||
|
||||
await PostRepo.create({ values: { title: 't1' } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
let [execution] = await workflow.getExecutions();
|
||||
let [job] = await execution.getJobs();
|
||||
expect(job.status).toEqual(JOB_STATUS.PENDING);
|
||||
|
||||
await sleep(1000);
|
||||
|
||||
[execution] = await workflow.getExecutions();
|
||||
[job] = await execution.getJobs();
|
||||
expect(job.status).toEqual(JOB_STATUS.RESOLVED);
|
||||
expect(job.result).toMatch('timeout');
|
||||
});
|
||||
|
||||
it('response 400', async () => {
|
||||
await workflow.createNode({
|
||||
type: 'request',
|
||||
config: {
|
||||
url: url_400,
|
||||
method: 'GET',
|
||||
timeout: 1000,
|
||||
ignoreFail: false,
|
||||
} as RequestConfig,
|
||||
});
|
||||
|
||||
await PostRepo.create({ values: { title: 't1' } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
let [execution] = await workflow.getExecutions();
|
||||
let [job] = await execution.getJobs();
|
||||
expect(job.status).toEqual(JOB_STATUS.PENDING);
|
||||
|
||||
await sleep(1000);
|
||||
|
||||
[execution] = await workflow.getExecutions();
|
||||
[job] = await execution.getJobs();
|
||||
expect(job.status).toEqual(JOB_STATUS.REJECTED);
|
||||
expect(job.result.status).toBe(400);
|
||||
});
|
||||
|
||||
it('response 400 ignoreFail', async () => {
|
||||
await workflow.createNode({
|
||||
type: 'request',
|
||||
config: {
|
||||
url: url_400,
|
||||
method: 'GET',
|
||||
timeout: 1000,
|
||||
ignoreFail: true,
|
||||
} as RequestConfig,
|
||||
});
|
||||
|
||||
await PostRepo.create({ values: { title: 't1' } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
let [execution] = await workflow.getExecutions();
|
||||
let [job] = await execution.getJobs();
|
||||
expect(job.status).toEqual(JOB_STATUS.PENDING);
|
||||
|
||||
await sleep(1000);
|
||||
|
||||
[execution] = await workflow.getExecutions();
|
||||
[job] = await execution.getJobs();
|
||||
expect(job.status).toEqual(JOB_STATUS.RESOLVED);
|
||||
expect(job.result.status).toBe(400);
|
||||
});
|
||||
|
||||
it('request with data', async () => {
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'request',
|
||||
config: {
|
||||
url: testUrl,
|
||||
method: 'POST',
|
||||
data: '{"title": "<%=ctx.data.title%>"}',
|
||||
} as RequestConfig,
|
||||
});
|
||||
|
||||
await PostRepo.create({ values: { title: 't1' } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
let [execution] = await workflow.getExecutions();
|
||||
let [job] = await execution.getJobs();
|
||||
expect(job.status).toEqual(JOB_STATUS.PENDING);
|
||||
|
||||
await sleep(1000);
|
||||
|
||||
[execution] = await workflow.getExecutions();
|
||||
[job] = await execution.getJobs();
|
||||
expect(job.status).toEqual(JOB_STATUS.RESOLVED);
|
||||
expect(JSON.parse(job.result.data)).toEqual({ title: 't1' });
|
||||
});
|
||||
});
|
||||
});
|
@ -43,7 +43,8 @@ export default function<T extends Instruction>(
|
||||
'query',
|
||||
'create',
|
||||
'update',
|
||||
'destroy'
|
||||
'destroy',
|
||||
'request'
|
||||
].reduce((result, key) => Object.assign(result, {
|
||||
[key]: requireModule(path.isAbsolute(key) ? key : path.join(__dirname, key))
|
||||
}), {});
|
||||
|
95
packages/plugins/workflow/src/server/instructions/request.ts
Normal file
95
packages/plugins/workflow/src/server/instructions/request.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { JOB_STATUS } from '../constants';
|
||||
import { render } from 'ejs';
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import _ from 'lodash';
|
||||
import { Instruction } from './index';
|
||||
|
||||
export interface Header {
|
||||
name: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export type RequestConfig = Pick<AxiosRequestConfig, 'url' | 'method' | 'data' | 'timeout'> & {
|
||||
headers: Array<Header>;
|
||||
ignoreFail: boolean;
|
||||
};
|
||||
|
||||
async function renderData(templateStr: string, vars: any): Promise<string> {
|
||||
if (_.isEmpty(templateStr)) {
|
||||
return '';
|
||||
}
|
||||
return render(templateStr, vars, { async: true });
|
||||
}
|
||||
|
||||
async function triggerResume(plugin, job, status, result) {
|
||||
job.set('status', status);
|
||||
job.set('result', result);
|
||||
return plugin.resume(job);
|
||||
}
|
||||
|
||||
export default class implements Instruction {
|
||||
async run(node, input, processor) {
|
||||
const templateVars = {
|
||||
node: processor.jobsMapByNodeId,
|
||||
ctx: processor.execution.context,
|
||||
};
|
||||
const requestConfig = node.config as RequestConfig;
|
||||
|
||||
// default headers
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
const { headers: headerArr = [], ignoreFail = false, method = 'POST', timeout = 5000 } = requestConfig;
|
||||
headerArr.forEach((header) => (headers[header.name] = header.value));
|
||||
|
||||
let url, data;
|
||||
try {
|
||||
url = await renderData(requestConfig.url, templateVars);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
throw new Error(`ejs can't render url, please check url format!${(e as Error)?.message}`);
|
||||
}
|
||||
try {
|
||||
data = await renderData(requestConfig.data, templateVars);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
throw new Error(`ejs can't render request data, please check request data format!${(e as Error)?.message}`);
|
||||
}
|
||||
|
||||
const job = await processor.saveJob({
|
||||
status: JOB_STATUS.PENDING,
|
||||
nodeId: node.id,
|
||||
});
|
||||
|
||||
const plugin = processor.options.plugin;
|
||||
axios
|
||||
.request({
|
||||
method,
|
||||
timeout,
|
||||
headers,
|
||||
url,
|
||||
data,
|
||||
})
|
||||
.then((resp) => {
|
||||
if (resp.status >= 200 && resp.status < 300) {
|
||||
triggerResume(plugin, job, JOB_STATUS.RESOLVED, resp.data);
|
||||
} else {
|
||||
triggerResume(plugin, job, JOB_STATUS.REJECTED, resp);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn(e);
|
||||
triggerResume(plugin, job, JOB_STATUS.REJECTED, (e as Error)?.message);
|
||||
});
|
||||
|
||||
return job;
|
||||
}
|
||||
|
||||
async resume(node, job, processor) {
|
||||
const { ignoreFail } = node.config as RequestConfig;
|
||||
if (ignoreFail) {
|
||||
job.set('status', JOB_STATUS.RESOLVED);
|
||||
}
|
||||
return job;
|
||||
}
|
||||
}
|
60
yarn.lock
60
yarn.lock
@ -5212,6 +5212,11 @@
|
||||
resolved "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-2.0.1.tgz#ffb6620d290624f3726aa362c0c8a4b44c8d7200"
|
||||
integrity sha512-TF8aoF5cHcLO7W7403blM7L1T+6NF3XMyN3fxyUolq2uOcFeicG/khQg/dGxiCJWoAcmYulYN7LYSRKO54IXaA==
|
||||
|
||||
"@types/ejs@^3.1.1":
|
||||
version "3.1.1"
|
||||
resolved "https://registry.npmmirror.com/@types/ejs/-/ejs-3.1.1.tgz#29c539826376a65e7f7d672d51301f37ed718f6d"
|
||||
integrity sha512-RQul5wEfY7BjWm0sYY86cmUN/pcXWGyVxWX93DFFJvcrxax5zKlieLwA3T77xJGwNcZW0YW6CYG70p1m8xPFmA==
|
||||
|
||||
"@types/estree@*":
|
||||
version "0.0.51"
|
||||
resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40"
|
||||
@ -7404,6 +7409,13 @@ brace-expansion@^1.1.7:
|
||||
balanced-match "^1.0.0"
|
||||
concat-map "0.0.1"
|
||||
|
||||
brace-expansion@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
|
||||
integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
|
||||
braces@^2.3.1, braces@^2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
|
||||
@ -7902,7 +7914,7 @@ chalk@^1.1.1, chalk@^1.1.3, chalk@~1.1.3:
|
||||
strip-ansi "^3.0.0"
|
||||
supports-color "^2.0.0"
|
||||
|
||||
chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1:
|
||||
chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
||||
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
|
||||
@ -8713,6 +8725,13 @@ copy-to-clipboard@^3.2.0:
|
||||
dependencies:
|
||||
toggle-selection "^1.0.6"
|
||||
|
||||
copy-to-clipboard@^3.3.1:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.npmmirror.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0"
|
||||
integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==
|
||||
dependencies:
|
||||
toggle-selection "^1.0.6"
|
||||
|
||||
copy-to@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmjs.org/copy-to/-/copy-to-2.0.1.tgz#2680fbb8068a48d08656b6098092bdafc906f4a5"
|
||||
@ -9936,6 +9955,13 @@ ee-first@1.1.1, ee-first@~1.1.1:
|
||||
resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
|
||||
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
|
||||
|
||||
ejs@^3.1.8:
|
||||
version "3.1.8"
|
||||
resolved "https://registry.npmmirror.com/ejs/-/ejs-3.1.8.tgz#758d32910c78047585c7ef1f92f9ee041c1c190b"
|
||||
integrity sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==
|
||||
dependencies:
|
||||
jake "^10.8.5"
|
||||
|
||||
electron-to-chromium@^1.3.886:
|
||||
version "1.3.896"
|
||||
resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.896.tgz#4a94efe4870b1687eafd5c378198a49da06e8a1b"
|
||||
@ -10930,6 +10956,13 @@ file-uri-to-path@2:
|
||||
resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-2.0.0.tgz#7b415aeba227d575851e0a5b0c640d7656403fba"
|
||||
integrity sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg==
|
||||
|
||||
filelist@^1.0.1:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.npmmirror.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5"
|
||||
integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==
|
||||
dependencies:
|
||||
minimatch "^5.0.1"
|
||||
|
||||
fill-range@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
|
||||
@ -13290,6 +13323,16 @@ istanbul-reports@^3.0.2:
|
||||
html-escaper "^2.0.0"
|
||||
istanbul-lib-report "^3.0.0"
|
||||
|
||||
jake@^10.8.5:
|
||||
version "10.8.5"
|
||||
resolved "https://registry.npmmirror.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46"
|
||||
integrity sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==
|
||||
dependencies:
|
||||
async "^3.2.3"
|
||||
chalk "^4.0.2"
|
||||
filelist "^1.0.1"
|
||||
minimatch "^3.0.4"
|
||||
|
||||
javascript-natural-sort@^0.7.1:
|
||||
version "0.7.1"
|
||||
resolved "https://registry.npmmirror.com/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz#f9e2303d4507f6d74355a73664d1440fb5a0ef59"
|
||||
@ -15718,6 +15761,13 @@ minimatch@^3.0.3, minimatch@^3.1.1, minimatch@^3.1.2:
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimatch@^5.0.1:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7"
|
||||
integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimist-options@4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619"
|
||||
@ -19233,6 +19283,14 @@ react-contenteditable@^3.3.6:
|
||||
fast-deep-equal "^3.1.3"
|
||||
prop-types "^15.7.1"
|
||||
|
||||
react-copy-to-clipboard@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.npmmirror.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz#09aae5ec4c62750ccb2e6421a58725eabc41255c"
|
||||
integrity sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==
|
||||
dependencies:
|
||||
copy-to-clipboard "^3.3.1"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
react-docgen-typescript-dumi-tmp@^1.22.1-0:
|
||||
version "1.22.1-0"
|
||||
resolved "https://registry.npmjs.org/react-docgen-typescript-dumi-tmp/-/react-docgen-typescript-dumi-tmp-1.22.1-0.tgz#6f452de05c5c114a6e1dd60b34930afaa7ae39a0"
|
||||
|
Loading…
Reference in New Issue
Block a user