@steedos-labs/plugin-workflow 3.0.95 → 3.0.97

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  waitForThing(window, 'antd').then(function(){
2
- var v = '3.0.95';
2
+ var v = '3.0.97';
3
3
  loadJs('/amis-renderer/amis-renderer.js?v=' + v);
4
4
  loadCss('/amis-renderer/amis-renderer.css?v=' + v)
5
5
  })
@@ -1,6 +1,7 @@
1
1
  const objectql = require("@steedos/objectql");
2
2
  const _ = require('underscore');
3
3
  const { _makeNewID, getCollection } = require('../utils/collection');
4
+ const { validateFlowSteps } = require('../utils/flowValidator');
4
5
 
5
6
  const steedosImport = {};
6
7
 
@@ -244,11 +245,12 @@ async function upgradeFlow(flowCome, userId, flowId) {
244
245
  updateObj.$set.name = flowCome['name'];
245
246
  updateObj.$set.name_formula = '';
246
247
  updateObj.$set.code_formula = '';
247
- updateObj.$set.is_valid = flowCome['is_valid'];
248
+ const flowValidation = validateFlowSteps(flowCome['current']['steps']);
249
+ updateObj.$set.is_valid = flowValidation.isValid;
248
250
  updateObj.$set.flowtype = flowCome['flowtype'];
249
251
  updateObj.$set.help_text = flowCome['help_text'];
250
252
  updateObj.$set.decription = flowCome['descriptions'];
251
- updateObj.$set.error_message = flowCome['error_message'];
253
+ updateObj.$set.error_message = flowValidation.errorMessage;
252
254
  updateObj.$set.modified = now;
253
255
  updateObj.$set.modified_by = userId;
254
256
  updateObj.$set.events = flowCome['events'] || '';
@@ -1676,8 +1676,23 @@ UUFlowManager.workflow_engine = async function (approve_from_client, current_use
1676
1676
  const t8 = Date.now();
1677
1677
  // Update instance
1678
1678
  const t8_1 = Date.now();
1679
+ // 当步骤发生流转时,current_step_name 的变更必须经由 objectql 写入,
1680
+ // 以触发 instances 的 afterUpdate(uuflowManager.triggerRecordInstanceQueue),
1681
+ // 否则 instance_record_queue 不会生成同步记录。
1682
+ // 这里先把 current_step_name 从原子 raw 写入中剥离(保留 $push traces 等原子性),
1683
+ // raw 写入后 DB 仍保留旧的 current_step_name,再由 objectql 单独应用新步骤名,
1684
+ // 使 afterUpdate 检测到 current_step_name 变化并触发同步。
1685
+ const nextStepNameForQueue = updateObj.$set ? updateObj.$set.current_step_name : undefined;
1686
+ const prevStepNameForQueue = updatedInstance.current_step_name;
1687
+ const stepNameChangedForQueue = nextStepNameForQueue !== undefined && nextStepNameForQueue !== prevStepNameForQueue;
1688
+ if (stepNameChangedForQueue) {
1689
+ delete updateObj.$set.current_step_name;
1690
+ }
1679
1691
  const instancesCollection = await getCollection('instances');
1680
1692
  await instancesCollection.updateOne({ _id: instance_id }, updateObj);
1693
+ if (stepNameChangedForQueue) {
1694
+ await getObject('instances').update(instance_id, { current_step_name: nextStepNameForQueue });
1695
+ }
1681
1696
  console.log(`[workflow/engine] [workflow_engine] second updateOne took ${Date.now() - t8_1}ms`);
1682
1697
 
1683
1698
  // Insert new instance tasks if needed
@@ -829,8 +829,32 @@ actions:
829
829
  navigate(0)
830
830
  },
831
831
  error: function (XMLHttpRequest, textStatus, errorThrown) {
832
+ var getAjaxErrorMessage = function (xhr, textStatus, errorThrown) {
833
+ var responseJSON = xhr && xhr.responseJSON;
834
+ if (typeof responseJSON === 'string' && responseJSON) {
835
+ return responseJSON;
836
+ }
837
+ if (responseJSON && typeof responseJSON === 'object') {
838
+ if (responseJSON.message) {
839
+ return responseJSON.message;
840
+ }
841
+ if (responseJSON.error) {
842
+ return responseJSON.error;
843
+ }
844
+ }
845
+ if (xhr && xhr.responseText) {
846
+ return xhr.responseText;
847
+ }
848
+ if (errorThrown) {
849
+ return errorThrown;
850
+ }
851
+ if (textStatus && textStatus !== 'error') {
852
+ return textStatus;
853
+ }
854
+ return '操作失败';
855
+ };
832
856
  SteedosUI.notification.error({
833
- message: XMLHttpRequest.responseJSON
857
+ message: getAjaxErrorMessage(XMLHttpRequest, textStatus, errorThrown)
834
858
  });
835
859
  }
836
860
  });
@@ -869,8 +893,32 @@ actions:
869
893
  navigate(0)
870
894
  },
871
895
  error: function (XMLHttpRequest, textStatus, errorThrown) {
896
+ var getAjaxErrorMessage = function (xhr, textStatus, errorThrown) {
897
+ var responseJSON = xhr && xhr.responseJSON;
898
+ if (typeof responseJSON === 'string' && responseJSON) {
899
+ return responseJSON;
900
+ }
901
+ if (responseJSON && typeof responseJSON === 'object') {
902
+ if (responseJSON.message) {
903
+ return responseJSON.message;
904
+ }
905
+ if (responseJSON.error) {
906
+ return responseJSON.error;
907
+ }
908
+ }
909
+ if (xhr && xhr.responseText) {
910
+ return xhr.responseText;
911
+ }
912
+ if (errorThrown) {
913
+ return errorThrown;
914
+ }
915
+ if (textStatus && textStatus !== 'error') {
916
+ return textStatus;
917
+ }
918
+ return '操作失败';
919
+ };
872
920
  SteedosUI.notification.error({
873
- message: XMLHttpRequest.responseJSON
921
+ message: getAjaxErrorMessage(XMLHttpRequest, textStatus, errorThrown)
874
922
  });
875
923
  }
876
924
  });
@@ -5,6 +5,7 @@ const bodyParser = require('body-parser');
5
5
  const steedosI18n = require("@steedos/i18n");
6
6
  const lodash = require('lodash');
7
7
  const { _makeNewID, getCollection } = require('../utils/collection');
8
+ const { validateFlowSteps } = require('../utils/flowValidator');
8
9
 
9
10
  const designerManager = require('../utils/designerManager');
10
11
 
@@ -589,11 +590,12 @@ router.put('/am/flows', async function (req, res) {
589
590
  updateObj.$set.name = flowCome['name'];
590
591
  updateObj.$set.name_formula = '';
591
592
  updateObj.$set.code_formula = '';
592
- updateObj.$set.is_valid = flowCome['is_valid'];
593
+ const flowValidation = validateFlowSteps(flowCome['current']['steps']);
594
+ updateObj.$set.is_valid = flowValidation.isValid;
593
595
  updateObj.$set.flowtype = flowCome['flowtype'];
594
596
  updateObj.$set.help_text = flowCome['help_text'];
595
597
  updateObj.$set.description = flowCome['descriptions'];
596
- updateObj.$set.error_message = flowCome['error_message'];
598
+ updateObj.$set.error_message = flowValidation.errorMessage;
597
599
  updateObj.$set.modified = now;
598
600
  updateObj.$set.modified_by = userId;
599
601
 
@@ -695,11 +697,11 @@ router.put('/am/flows/state', async function (req, res) {
695
697
  if (state === 'enabled') {
696
698
  // 流程启用前,校验其“指定历史步骤”属性中被引用的步骤是否存在且能被找到(仅限于流程的最新版)
697
699
  let checkStepIds = [];
698
- _.each(flow.current.steps, function (step) {
700
+ _.each(flow.current.steps || [], function (step) {
699
701
  checkStepIds.push(step._id);
700
702
  })
701
703
 
702
- _.each(flow.current.steps, function (step) {
704
+ _.each(flow.current.steps || [], function (step) {
703
705
  if (step.deal_type === 'specifyStepUser' || step.deal_type === 'specifyStepRole') {
704
706
  if (!step.approver_step || !checkStepIds.includes(step.approver_step)) {
705
707
  throw new Error('流程中的指定步骤不存在');
@@ -707,6 +709,19 @@ router.put('/am/flows/state', async function (req, res) {
707
709
  }
708
710
  })
709
711
 
712
+ const flowValidation = validateFlowSteps(flow.current.steps);
713
+ if (!flowValidation.isValid) {
714
+ await flowCollection.updateOne({_id: flowId}, {
715
+ $set: {
716
+ is_valid: false,
717
+ error_message: flowValidation.errorMessage,
718
+ modified: now,
719
+ modified_by: userId
720
+ }
721
+ });
722
+ throw new Error(flowValidation.errorMessage || '流程不合法');
723
+ }
724
+
710
725
  // 如果 流程对应表单 是停用的 则启用
711
726
  if (form.state === 'disabled') {
712
727
  let formUpdateObj = {
@@ -721,9 +736,6 @@ router.put('/am/flows/state', async function (req, res) {
721
736
  updatedForms.push(await formCollection.findOne({_id: formId}));
722
737
  }
723
738
 
724
- if (!flow.is_valid) {
725
- throw new Error('流程不合法');
726
- }
727
739
  if (!['new', 'modify', 'delete'].includes(flow.flowtype)) {
728
740
  throw new Error('FlowType值必须是new、modify、delete其中之一');
729
741
  }
@@ -732,6 +744,8 @@ router.put('/am/flows/state', async function (req, res) {
732
744
  }
733
745
 
734
746
  flowUpdateObj.$set['state'] = 'enabled';
747
+ flowUpdateObj.$set['is_valid'] = true;
748
+ flowUpdateObj.$set['error_message'] = '';
735
749
  flowUpdateObj.$set['current.modified'] = now;
736
750
  flowUpdateObj.$set['current.start_date'] = now;
737
751
  flowUpdateObj.$set['current.modified_by'] = userId;
@@ -1221,4 +1235,4 @@ router.post('/am/forms/addFieldsFromObject', async function (req, res) {
1221
1235
  }
1222
1236
  })
1223
1237
 
1224
- exports.default = router;
1238
+ exports.default = router;
@@ -0,0 +1,36 @@
1
+ const assert = require('assert');
2
+ const { getAjaxErrorMessage } = require('../utils/ajaxErrorMessage');
3
+
4
+ console.log('[ajax-error-message] running tests...');
5
+
6
+ assert.strictEqual(
7
+ getAjaxErrorMessage({ responseText: '流程不合法' }, 'error', 'Internal Server Error'),
8
+ '流程不合法'
9
+ );
10
+
11
+ assert.strictEqual(
12
+ getAjaxErrorMessage({ responseJSON: { message: '具体错误' }, responseText: 'fallback' }, 'error', 'Internal Server Error'),
13
+ '具体错误'
14
+ );
15
+
16
+ assert.strictEqual(
17
+ getAjaxErrorMessage({ responseJSON: { error: '错误字段' } }, 'error', 'Internal Server Error'),
18
+ '错误字段'
19
+ );
20
+
21
+ assert.strictEqual(
22
+ getAjaxErrorMessage({ responseJSON: '字符串错误' }, 'error', 'Internal Server Error'),
23
+ '字符串错误'
24
+ );
25
+
26
+ assert.strictEqual(
27
+ getAjaxErrorMessage({}, 'error', 'Internal Server Error'),
28
+ 'Internal Server Error'
29
+ );
30
+
31
+ assert.strictEqual(
32
+ getAjaxErrorMessage({}, 'error', ''),
33
+ '操作失败'
34
+ );
35
+
36
+ console.log('[ajax-error-message] all tests passed');
@@ -0,0 +1,73 @@
1
+ const assert = require('assert');
2
+ const { validateFlowSteps } = require('../utils/flowValidator');
3
+
4
+ function validSteps() {
5
+ return [
6
+ {
7
+ _id: 'start',
8
+ name: '开始',
9
+ step_type: 'start',
10
+ lines: [{ _id: 'l1', state: 'submitted', to_step: 'approve' }]
11
+ },
12
+ {
13
+ _id: 'approve',
14
+ name: '审批',
15
+ step_type: 'sign',
16
+ deal_type: 'pickupAtRuntime',
17
+ lines: [{ _id: 'l2', state: 'approved', to_step: 'end' }]
18
+ },
19
+ {
20
+ _id: 'end',
21
+ name: '结束',
22
+ step_type: 'end',
23
+ lines: []
24
+ }
25
+ ];
26
+ }
27
+
28
+ function assertInvalid(steps, expectedText) {
29
+ const result = validateFlowSteps(steps);
30
+ assert.strictEqual(result.isValid, false);
31
+ assert.ok(result.errorMessage.includes(expectedText), result.errorMessage);
32
+ }
33
+
34
+ console.log('[flow-validator] running tests...');
35
+
36
+ let result = validateFlowSteps(validSteps());
37
+ assert.deepStrictEqual(result, {
38
+ isValid: true,
39
+ errorMessage: '',
40
+ errors: []
41
+ });
42
+
43
+ const noOutLine = validSteps();
44
+ noOutLine[1].lines = [];
45
+ assertInvalid(noOutLine, '审批:没有出线');
46
+
47
+ const missingTarget = validSteps();
48
+ missingTarget[1].lines[0].to_step = 'missing';
49
+ assertInvalid(missingTarget, '审批:连线目标步骤不存在');
50
+
51
+ const duplicateName = validSteps();
52
+ duplicateName[2].name = '审批';
53
+ assertInvalid(duplicateName, '步骤名称重复');
54
+
55
+ const conditionMissingExpression = validSteps();
56
+ conditionMissingExpression.splice(1, 0, {
57
+ _id: 'condition',
58
+ name: '条件',
59
+ step_type: 'condition',
60
+ lines: [
61
+ { _id: 'c1', state: 'approved', to_step: 'approve', condition: '' },
62
+ { _id: 'c2', state: 'approved', to_step: 'end', condition: 'true' }
63
+ ]
64
+ });
65
+ conditionMissingExpression[0].lines[0].to_step = 'condition';
66
+ assertInvalid(conditionMissingExpression, '条件:到「审批」的出线未设置条件');
67
+
68
+ const specifyStepMissing = validSteps();
69
+ specifyStepMissing[1].deal_type = 'specifyStepUser';
70
+ specifyStepMissing[1].approver_step = 'missing';
71
+ assertInvalid(specifyStepMissing, '审批:指定历史步骤不存在');
72
+
73
+ console.log('[flow-validator] all tests passed');
@@ -0,0 +1,28 @@
1
+ function getAjaxErrorMessage(xhr, textStatus, errorThrown) {
2
+ const responseJSON = xhr && xhr.responseJSON;
3
+ if (typeof responseJSON === 'string' && responseJSON) {
4
+ return responseJSON;
5
+ }
6
+ if (responseJSON && typeof responseJSON === 'object') {
7
+ if (responseJSON.message) {
8
+ return responseJSON.message;
9
+ }
10
+ if (responseJSON.error) {
11
+ return responseJSON.error;
12
+ }
13
+ }
14
+ if (xhr && xhr.responseText) {
15
+ return xhr.responseText;
16
+ }
17
+ if (errorThrown) {
18
+ return errorThrown;
19
+ }
20
+ if (textStatus && textStatus !== 'error') {
21
+ return textStatus;
22
+ }
23
+ return '操作失败';
24
+ }
25
+
26
+ module.exports = {
27
+ getAjaxErrorMessage
28
+ };
@@ -0,0 +1,162 @@
1
+ const _ = require('underscore');
2
+
3
+ const STEP_TYPES_NEED_DEAL_TYPE = ['submit', 'sign', 'counterSign', 'notify'];
4
+
5
+ function getStepId(step) {
6
+ return step && (step._id || step.id);
7
+ }
8
+
9
+ function getStepName(step) {
10
+ const name = step && step.name;
11
+ return typeof name === 'string' && name.trim() ? name.trim() : getStepId(step) || '未命名步骤';
12
+ }
13
+
14
+ function getLines(step) {
15
+ return Array.isArray(step && step.lines) ? step.lines : [];
16
+ }
17
+
18
+ function pushError(errors, step, field, message, extra) {
19
+ errors.push(Object.assign({
20
+ stepId: getStepId(step),
21
+ stepName: getStepName(step),
22
+ field,
23
+ message: `${getStepName(step)}:${message}`
24
+ }, extra || {}));
25
+ }
26
+
27
+ function validateDealType(errors, step, stepIds) {
28
+ const stepType = step.step_type || 'sign';
29
+ const dealType = step.deal_type || '';
30
+
31
+ if (STEP_TYPES_NEED_DEAL_TYPE.includes(stepType) && !dealType) {
32
+ pushError(errors, step, 'deal_type', '未选择处理人身份');
33
+ return;
34
+ }
35
+
36
+ if (dealType === 'specifyUser' && _.isEmpty(step.approver_users)) {
37
+ pushError(errors, step, 'approver_users', '指定人员不能为空');
38
+ }
39
+ if (dealType === 'specifyOrg' && _.isEmpty(step.approver_orgs)) {
40
+ pushError(errors, step, 'approver_orgs', '指定部门不能为空');
41
+ }
42
+ if ((dealType === 'applicantRole' || dealType === 'userFieldRole' || dealType === 'orgFieldRole' || dealType === 'specifyStepRole') && _.isEmpty(step.approver_roles)) {
43
+ pushError(errors, step, 'approver_roles', '指定审批岗位不能为空');
44
+ }
45
+ if (dealType === 'hrRole' && _.isEmpty(step.approver_hr_roles)) {
46
+ pushError(errors, step, 'approver_hr_roles', '指定角色不能为空');
47
+ }
48
+ if ((dealType === 'userField' || dealType === 'userFieldRole') && !step.approver_user_field) {
49
+ pushError(errors, step, 'approver_user_field', '指定人员字段不能为空');
50
+ }
51
+ if ((dealType === 'orgField' || dealType === 'orgFieldRole') && !step.approver_org_field) {
52
+ pushError(errors, step, 'approver_org_field', '指定部门字段不能为空');
53
+ }
54
+ if ((dealType === 'specifyStepUser' || dealType === 'specifyStepRole') && (!step.approver_step || !stepIds.has(step.approver_step))) {
55
+ pushError(errors, step, 'approver_step', '指定历史步骤不存在');
56
+ }
57
+ }
58
+
59
+ function validateFlowSteps(steps) {
60
+ const errors = [];
61
+
62
+ if (!Array.isArray(steps) || steps.length === 0) {
63
+ return {
64
+ isValid: false,
65
+ errorMessage: '流程的步骤不能为空',
66
+ errors: [{
67
+ field: 'steps',
68
+ message: '流程的步骤不能为空'
69
+ }]
70
+ };
71
+ }
72
+
73
+ const stepIds = new Set();
74
+ const nameMap = new Map();
75
+ const incomingMap = new Map();
76
+
77
+ steps.forEach(function (step) {
78
+ const stepId = getStepId(step);
79
+ const stepName = getStepName(step);
80
+ if (!stepId) {
81
+ pushError(errors, step, '_id', '步骤ID不能为空');
82
+ } else if (stepIds.has(stepId)) {
83
+ pushError(errors, step, '_id', '步骤ID重复');
84
+ } else {
85
+ stepIds.add(stepId);
86
+ }
87
+
88
+ if (!step.name || !String(step.name).trim()) {
89
+ pushError(errors, step, 'name', '步骤名称不能为空');
90
+ }
91
+ nameMap.set(stepName, (nameMap.get(stepName) || 0) + 1);
92
+ });
93
+
94
+ steps.forEach(function (step) {
95
+ const stepId = getStepId(step);
96
+ getLines(step).forEach(function (line) {
97
+ if (line && line.to_step) {
98
+ incomingMap.set(line.to_step, (incomingMap.get(line.to_step) || 0) + 1);
99
+ }
100
+ if (!line || !line.to_step || !stepIds.has(line.to_step)) {
101
+ pushError(errors, step, 'lines', '连线目标步骤不存在', { lineId: line && (line._id || line.id) });
102
+ }
103
+ if (line && line.to_step === stepId) {
104
+ pushError(errors, step, 'lines', '不能连接到自身', { lineId: line && (line._id || line.id) });
105
+ }
106
+ if (line && !line.state) {
107
+ pushError(errors, step, 'state', '连线类型不能为空', { lineId: line && (line._id || line.id) });
108
+ }
109
+ });
110
+ });
111
+
112
+ nameMap.forEach(function (count, name) {
113
+ if (count > 1) {
114
+ errors.push({
115
+ field: 'name',
116
+ stepName: name,
117
+ message: `步骤名称重复:${name}`
118
+ });
119
+ }
120
+ });
121
+
122
+ steps.forEach(function (step) {
123
+ const stepType = step.step_type || 'sign';
124
+ const lines = getLines(step);
125
+ const incomingCount = incomingMap.get(getStepId(step)) || 0;
126
+
127
+ if (stepType !== 'end' && lines.length === 0) {
128
+ pushError(errors, step, 'lines', '没有出线');
129
+ }
130
+ if (stepType === 'end' && lines.length > 0) {
131
+ pushError(errors, step, 'lines', '结束节点不能有出线');
132
+ }
133
+ if (stepType !== 'start' && incomingCount === 0) {
134
+ pushError(errors, step, 'lines', '没有入线');
135
+ }
136
+ if (stepType === 'condition') {
137
+ if (lines.length < 2) {
138
+ pushError(errors, step, 'condition_lines', '条件节点至少需要两条出线');
139
+ }
140
+ lines.forEach(function (line) {
141
+ if (!String((line && line.condition) || '').trim()) {
142
+ const target = steps.find(function (item) {
143
+ return getStepId(item) === line.to_step;
144
+ });
145
+ pushError(errors, step, 'condition_lines', `到「${target ? getStepName(target) : line.to_step || '未知节点'}」的出线未设置条件`, { lineId: line && (line._id || line.id) });
146
+ }
147
+ });
148
+ }
149
+
150
+ validateDealType(errors, step, stepIds);
151
+ });
152
+
153
+ return {
154
+ isValid: errors.length === 0,
155
+ errorMessage: errors.map(function (error) { return error.message; }).join('\n'),
156
+ errors
157
+ };
158
+ }
159
+
160
+ module.exports = {
161
+ validateFlowSteps
162
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steedos-labs/plugin-workflow",
3
- "version": "3.0.95",
3
+ "version": "3.0.97",
4
4
  "main": "package.service.js",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -25,7 +25,9 @@
25
25
  "convert-templates": "node convert-templates.js",
26
26
  "test:formula-compat": "node main/default/test/test_formula_compat.js",
27
27
  "test:approve-values": "node main/default/test/test_getApproveValues.js",
28
- "test:skip-handler-error": "node main/default/test/test_handleSkipProcessed_handlerError.js"
28
+ "test:skip-handler-error": "node main/default/test/test_handleSkipProcessed_handlerError.js",
29
+ "test:flow-validator": "node main/default/test/test_flow_validator.js",
30
+ "test:ajax-error-message": "node main/default/test/test_ajax_error_message.js"
29
31
  },
30
32
  "dependencies": {
31
33
  "graphql-parse-resolve-info": "^4.12.3",