@pxp-nd/flow 1.1.31 → 1.1.32
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.
- package/dist/controllers/Flow.js +13 -21
- package/dist/controllers/FlowInstance.js +2 -2
- package/dist/controllers/Node.js +18 -12
- package/dist/controllers/NodeInstance.js +6 -4
- package/dist/helpers/flowFilter.d.ts +13 -0
- package/dist/helpers/flowFilter.js +56 -0
- package/dist/helpers/sessionVendor.d.ts +13 -0
- package/dist/helpers/sessionVendor.js +28 -0
- package/dist/helpers/sqlIdent.d.ts +17 -0
- package/dist/helpers/sqlIdent.js +34 -0
- package/package.json +13 -3
- package/test/sql-ident.test.ts +168 -0
- package/test/tsconfig.json +9 -0
package/dist/controllers/Flow.js
CHANGED
|
@@ -68,6 +68,7 @@ const NodeConnection_1 = __importDefault(require("../entity/NodeConnection"));
|
|
|
68
68
|
const Flow_1 = __importDefault(require("../entity/Flow"));
|
|
69
69
|
const FlowInstance_1 = __importDefault(require("../entity/FlowInstance"));
|
|
70
70
|
const Node_2 = __importDefault(require("./Node"));
|
|
71
|
+
const flowFilter_1 = require("../helpers/flowFilter");
|
|
71
72
|
const axios_1 = __importDefault(require("axios"));
|
|
72
73
|
//import {configFacebookStrategy} from "./passport-facebook";
|
|
73
74
|
const FlowNdApiDocs_1 = require("../helpers/FlowNdApiDocs");
|
|
@@ -192,14 +193,10 @@ let Flow = class Flow extends core_1.Controller {
|
|
|
192
193
|
dataFlow.modifiedBy = this.user.username;
|
|
193
194
|
const updFlow = await (0, core_1.__)(manager.save(dataFlow));
|
|
194
195
|
if (updFlow) {
|
|
195
|
-
|
|
196
|
-
is_active = 'N' WHERE flow_id = ${updFlow.flowId}`;
|
|
197
|
-
await manager.query(flowInstance);
|
|
196
|
+
await manager.query(`UPDATE twf_flow_instance SET is_active = 'N' WHERE flow_id = ?`, [updFlow.flowId]);
|
|
198
197
|
const nodesToInact = await manager.find(Node_1.default, { flowId: updFlow.flowId, isActive: true });
|
|
199
198
|
if (nodesToInact.length > 0) {
|
|
200
|
-
|
|
201
|
-
is_active = 'N' WHERE flow_id = ${updFlow.flowId}`;
|
|
202
|
-
await manager.query(flowNode);
|
|
199
|
+
await manager.query(`UPDATE twf_node SET is_active = 'N' WHERE flow_id = ?`, [updFlow.flowId]);
|
|
203
200
|
const nodesToInact = await manager.find(Node_1.default, { flowId: updFlow.flowId });
|
|
204
201
|
const originalNodeIds = nodesToInact.map((node) => node.nodeId);
|
|
205
202
|
const connectionsToInact = await manager.find(NodeConnection_1.default, {
|
|
@@ -207,9 +204,7 @@ let Flow = class Flow extends core_1.Controller {
|
|
|
207
204
|
});
|
|
208
205
|
const originalNodeCIds = connectionsToInact.map((node) => node.nodeConnectionId);
|
|
209
206
|
if (originalNodeCIds.length > 0) {
|
|
210
|
-
|
|
211
|
-
is_active = 'N' WHERE node_connection_id in ( ${originalNodeCIds})`;
|
|
212
|
-
await manager.query(flowNodeC);
|
|
207
|
+
await manager.query(`UPDATE twf_node_connection SET is_active = 'N' WHERE node_connection_id IN (?)`, [originalNodeCIds]);
|
|
213
208
|
}
|
|
214
209
|
}
|
|
215
210
|
return { success: true };
|
|
@@ -328,9 +323,9 @@ let Flow = class Flow extends core_1.Controller {
|
|
|
328
323
|
});
|
|
329
324
|
if (connectionsToDel) {
|
|
330
325
|
const originalNodeCIds = connectionsToDel.map((node) => node.nodeConnectionId);
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
326
|
+
if (originalNodeCIds.length > 0) {
|
|
327
|
+
await manager.query(`DELETE FROM twf_node_connection WHERE node_connection_id IN (?)`, [originalNodeCIds]);
|
|
328
|
+
}
|
|
334
329
|
}
|
|
335
330
|
}
|
|
336
331
|
}
|
|
@@ -349,9 +344,9 @@ let Flow = class Flow extends core_1.Controller {
|
|
|
349
344
|
allowModify = true;
|
|
350
345
|
//If there are some nodes in node_connections after delete all records sent, to inactive node
|
|
351
346
|
let connectNoDel = await (0, typeorm_1.getManager)().query(`SELECT n.node_id
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
347
|
+
FROM twf_node_connection nd
|
|
348
|
+
INNER JOIN twf_node n ON n.node_id = nd.node_id_child
|
|
349
|
+
WHERE n.node_id = ?`, [nodeDelId]);
|
|
355
350
|
const currentNodesIds = connectNoDel.map((record) => record.node_id);
|
|
356
351
|
// Get nodes to remove
|
|
357
352
|
const nodesToRemove = currentNodesIds.filter((nodeId) => !taskIds.includes(nodeId.toString()));
|
|
@@ -359,10 +354,7 @@ let Flow = class Flow extends core_1.Controller {
|
|
|
359
354
|
if (nodesToRemove.length > 0) {
|
|
360
355
|
const delNodeConnect = await manager.delete(NodeConnection_1.default, { nodeIdChild: nodesToRemove[0] });
|
|
361
356
|
if (delNodeConnect) {
|
|
362
|
-
|
|
363
|
-
SET is_active = 'N'
|
|
364
|
-
WHERE node_id = ${nodesToRemove[0]}`;
|
|
365
|
-
await manager.query(dNode);
|
|
357
|
+
await manager.query(`UPDATE twf_node SET is_active = 'N' WHERE node_id = ?`, [nodesToRemove[0]]);
|
|
366
358
|
}
|
|
367
359
|
}
|
|
368
360
|
if (flowIdDel.action.actionType.name === 'CHAINED FLOW') {
|
|
@@ -842,7 +834,7 @@ let Flow = class Flow extends core_1.Controller {
|
|
|
842
834
|
const applyDynamicFilters = (queryBuilder, filters, logicOperator) => {
|
|
843
835
|
filters.forEach((filter, index) => {
|
|
844
836
|
const { field, operator, value } = filter;
|
|
845
|
-
const queryField = `f.${field}`;
|
|
837
|
+
const queryField = `f.${(0, flowFilter_1.flowFilterColumn)(field)}`;
|
|
846
838
|
const parameterName = `filterValue${index}`;
|
|
847
839
|
let condition;
|
|
848
840
|
switch (operator) {
|
|
@@ -1006,7 +998,7 @@ let Flow = class Flow extends core_1.Controller {
|
|
|
1006
998
|
const applyDynamicFilters = (queryBuilder, filters, logicOperator) => {
|
|
1007
999
|
filters.forEach((filter, index) => {
|
|
1008
1000
|
const { field, operator, value } = filter;
|
|
1009
|
-
const queryField = `f.${field}`;
|
|
1001
|
+
const queryField = `f.${(0, flowFilter_1.flowFilterColumn)(field)}`;
|
|
1010
1002
|
const parameterName = `filterValue${index}`;
|
|
1011
1003
|
let condition;
|
|
1012
1004
|
switch (operator) {
|
|
@@ -41,6 +41,7 @@ const Flow_1 = __importDefault(require("../entity/Flow"));
|
|
|
41
41
|
const Action_1 = __importDefault(require("../entity/Action"));
|
|
42
42
|
const FlowLog_1 = __importDefault(require("../entity/FlowLog"));
|
|
43
43
|
const dbLock_1 = require("../helpers/dbLock");
|
|
44
|
+
const sqlIdent_1 = require("../helpers/sqlIdent");
|
|
44
45
|
class FlowInstance extends core_1.Controller {
|
|
45
46
|
async processPending(params, manager) {
|
|
46
47
|
let flowInstanceIdProcessing = -1;
|
|
@@ -89,8 +90,7 @@ class FlowInstance extends core_1.Controller {
|
|
|
89
90
|
for (const n of node) {
|
|
90
91
|
//get data of view
|
|
91
92
|
const { action: { originName, originKey }, } = n;
|
|
92
|
-
const
|
|
93
|
-
const resExecuteView = await (0, typeorm_1.getManager)().query(executeView);
|
|
93
|
+
const resExecuteView = await (0, sqlIdent_1.selectByIdent)((0, typeorm_1.getManager)(), originName, originKey, newEvent.dataId);
|
|
94
94
|
const flow = await (0, core_1.__)(Flow_1.default.findOne({ where: { flowId: n.flowId, isActive: 1, status: 'on' } }));
|
|
95
95
|
if (flow && flow.vendorId == resExecuteView[0].vendor_id &&
|
|
96
96
|
(await this.checkConditions(resExecuteView[0], n.flowId, newEvent.actionId))) {
|
package/dist/controllers/Node.js
CHANGED
|
@@ -37,11 +37,12 @@ const OriginName_1 = __importDefault(require("../entity/OriginName"));
|
|
|
37
37
|
const axios_1 = __importDefault(require("axios"));
|
|
38
38
|
const Flow_1 = __importDefault(require("../entity/Flow"));
|
|
39
39
|
const FlowNdApiDocs_1 = require("../helpers/FlowNdApiDocs");
|
|
40
|
+
const sqlIdent_1 = require("../helpers/sqlIdent");
|
|
41
|
+
const sessionVendor_1 = require("../helpers/sessionVendor");
|
|
40
42
|
let Node = class Node extends core_1.Controller {
|
|
41
43
|
async getNodeData(node, dataId, manager) {
|
|
42
44
|
const { action: { originName, originKey } } = node;
|
|
43
|
-
const
|
|
44
|
-
const resExecuteView = await (0, core_1.__)(manager.query(executeView));
|
|
45
|
+
const resExecuteView = await (0, core_1.__)((0, sqlIdent_1.selectByIdent)(manager, originName, originKey, dataId));
|
|
45
46
|
return JSON.stringify(resExecuteView[0]);
|
|
46
47
|
}
|
|
47
48
|
async add(params, manager) {
|
|
@@ -119,7 +120,14 @@ let Node = class Node extends core_1.Controller {
|
|
|
119
120
|
if (!Number.isInteger(nid) || nid < 1) {
|
|
120
121
|
throw new core_1.PxpError(400, 'nodeId is required and must be a positive integer');
|
|
121
122
|
}
|
|
122
|
-
const { nodeId, flowId
|
|
123
|
+
const { nodeId, flowId } = params;
|
|
124
|
+
const requestedSubstitutions = params.substitutionsSchemaJson && typeof params.substitutionsSchemaJson === 'object'
|
|
125
|
+
? { ...params.substitutionsSchemaJson }
|
|
126
|
+
: {};
|
|
127
|
+
const sessionVendor = (0, sessionVendor_1.harmonyVendorId)(this.user);
|
|
128
|
+
const substitutionsSchemaJson = sessionVendor != null
|
|
129
|
+
? { ...requestedSubstitutions, vendorId: sessionVendor }
|
|
130
|
+
: requestedSubstitutions;
|
|
123
131
|
const tNodeData = (0, core_1.__)(Node_1.default.findOne(nodeId));
|
|
124
132
|
const nodeData = await tNodeData;
|
|
125
133
|
const tNodeOrigin = await (0, core_1.__)(Node_1.default.findOne({
|
|
@@ -261,17 +269,15 @@ let Node = class Node extends core_1.Controller {
|
|
|
261
269
|
};
|
|
262
270
|
}
|
|
263
271
|
async executeActionJson(params, manager) {
|
|
264
|
-
const { nodeId,
|
|
265
|
-
|
|
272
|
+
const { nodeId, fromValues } = params;
|
|
273
|
+
const nodeRow = await (0, core_1.__)(Node_1.default.findOne({ where: { nodeId }, relations: ['action'] }));
|
|
274
|
+
if (!nodeRow || !nodeRow.action) {
|
|
275
|
+
throw new core_1.PxpError(400, 'Invalid origin');
|
|
276
|
+
}
|
|
266
277
|
const getParameterizedNodeData = await (0, core_1.__)(this.getParameterizedNode({ nodeId: nodeId }, manager));
|
|
267
278
|
const { mergeValues } = getParameterizedNodeData;
|
|
268
|
-
const
|
|
269
|
-
const
|
|
270
|
-
const originValue = resValueObject[fromValues];
|
|
271
|
-
const executeViewString = `select * from ${originName} where ${originKey} = ${originValue}`;
|
|
272
|
-
const resExecuteView = await (0, typeorm_1.getManager)().query(executeViewString);
|
|
273
|
-
/*const executeView = `select * from ${originName} where ${originKey} = ${newEvent.dataId}`;
|
|
274
|
-
const resExecuteView = await getManager().query(executeView);*/
|
|
279
|
+
const originValue = mergeValues ? mergeValues[fromValues] : undefined;
|
|
280
|
+
const resExecuteView = await (0, sqlIdent_1.selectByIdent)((0, typeorm_1.getManager)(), nodeRow.action.originName, nodeRow.action.originKey, originValue);
|
|
275
281
|
return {
|
|
276
282
|
...resExecuteView[0]
|
|
277
283
|
};
|
|
@@ -42,18 +42,20 @@ const Action_1 = __importDefault(require("../entity/Action"));
|
|
|
42
42
|
const lodash_1 = __importDefault(require("lodash"));
|
|
43
43
|
const FlowInstance_1 = __importDefault(require("../entity/FlowInstance"));
|
|
44
44
|
const dbLock_1 = require("../helpers/dbLock");
|
|
45
|
+
const sqlIdent_1 = require("../helpers/sqlIdent");
|
|
45
46
|
//TODO replace this flow-nd need tobe independent library
|
|
46
47
|
const common_1 = require("@pxp-nd/common");
|
|
47
48
|
let NodeInstance = class NodeInstance extends core_1.Controller {
|
|
48
49
|
async RecursiveInstance(params, manager) {
|
|
49
|
-
var _a;
|
|
50
|
+
var _a, _b, _c, _d, _e;
|
|
50
51
|
//https://lodash.com/docs/4.17.15#template
|
|
51
52
|
lodash_1.default.templateSettings.interpolate = /{{([\s\S]+?)}}/g;
|
|
52
53
|
//todo after this we need to get the json variables and execute the view and conditions configured in the database
|
|
53
54
|
const { node, flowInstance, eventNode } = params;
|
|
54
55
|
try {
|
|
55
|
-
const
|
|
56
|
-
const
|
|
56
|
+
const originName = (_b = (_a = node === null || node === void 0 ? void 0 : node.action) === null || _a === void 0 ? void 0 : _a.originName) !== null && _b !== void 0 ? _b : flowInstance.originName;
|
|
57
|
+
const originKey = (_d = (_c = node === null || node === void 0 ? void 0 : node.action) === null || _c === void 0 ? void 0 : _c.originKey) !== null && _d !== void 0 ? _d : flowInstance.originKey;
|
|
58
|
+
const resExecuteView = await (0, core_1.__)((0, sqlIdent_1.selectByIdent)((0, typeorm_1.getManager)(), originName, originKey, flowInstance.dataId));
|
|
57
59
|
const resultFromOrigin = resExecuteView[0];
|
|
58
60
|
//let mergeJson = {};
|
|
59
61
|
let mergeJson = {};
|
|
@@ -94,7 +96,7 @@ let NodeInstance = class NodeInstance extends core_1.Controller {
|
|
|
94
96
|
if (nodeInstanceId) {
|
|
95
97
|
mergeJson.nodeInstanceId = nodeInstanceId;
|
|
96
98
|
}
|
|
97
|
-
if (!!((
|
|
99
|
+
if (!!((_e = node === null || node === void 0 ? void 0 : node.action) === null || _e === void 0 ? void 0 : _e.controllerFunction)) {
|
|
98
100
|
const config = {
|
|
99
101
|
method: "post",
|
|
100
102
|
url: `http://localhost:${process.env.PORT}/api/${node.action.controllerFunction}`,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*****************************************************************************
|
|
2
|
+
* Copyright(c) 2026 Qorus Inc
|
|
3
|
+
* All rights reserved
|
|
4
|
+
*****************************************************************************
|
|
5
|
+
* NAME: flowFilter.ts
|
|
6
|
+
* DEVELOPER: Rensi Arteaga
|
|
7
|
+
* DESCRIPTION: Whitelist of flow list filter columns. Client field names are not interpolated.
|
|
8
|
+
* REVISIONS:
|
|
9
|
+
* Date Change ID Author Description
|
|
10
|
+
* ----------- ----------- -------------- ------------------------------------
|
|
11
|
+
* 10-Nov-2026 Rensi Arteaga Created
|
|
12
|
+
******************************************************************************/
|
|
13
|
+
export declare function flowFilterColumn(field: unknown): string;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*****************************************************************************
|
|
3
|
+
* Copyright(c) 2026 Qorus Inc
|
|
4
|
+
* All rights reserved
|
|
5
|
+
*****************************************************************************
|
|
6
|
+
* NAME: flowFilter.ts
|
|
7
|
+
* DEVELOPER: Rensi Arteaga
|
|
8
|
+
* DESCRIPTION: Whitelist of flow list filter columns. Client field names are not interpolated.
|
|
9
|
+
* REVISIONS:
|
|
10
|
+
* Date Change ID Author Description
|
|
11
|
+
* ----------- ----------- -------------- ------------------------------------
|
|
12
|
+
* 10-Nov-2026 Rensi Arteaga Created
|
|
13
|
+
******************************************************************************/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.flowFilterColumn = void 0;
|
|
16
|
+
const core_1 = require("@pxp-nd/core");
|
|
17
|
+
const FLOW_FILTER_COLUMNS = {
|
|
18
|
+
flowId: 'flow_id',
|
|
19
|
+
flow_id: 'flow_id',
|
|
20
|
+
vendorId: 'vendor_id',
|
|
21
|
+
vendor_id: 'vendor_id',
|
|
22
|
+
code: 'code',
|
|
23
|
+
name: 'name',
|
|
24
|
+
enabled: 'enabled',
|
|
25
|
+
type: 'type',
|
|
26
|
+
createdBy: 'created_by',
|
|
27
|
+
created_by: 'created_by',
|
|
28
|
+
modifiedBy: 'modified_by',
|
|
29
|
+
modified_by: 'modified_by',
|
|
30
|
+
createdAt: 'created_at',
|
|
31
|
+
created_at: 'created_at',
|
|
32
|
+
modifiedAt: 'modified_at',
|
|
33
|
+
modified_at: 'modified_at',
|
|
34
|
+
isActive: 'is_active',
|
|
35
|
+
is_active: 'is_active',
|
|
36
|
+
description: 'description',
|
|
37
|
+
icon: 'icon',
|
|
38
|
+
status: 'status',
|
|
39
|
+
templateType: 'template_type',
|
|
40
|
+
template_type: 'template_type',
|
|
41
|
+
flowType: 'flow_type',
|
|
42
|
+
flow_type: 'flow_type',
|
|
43
|
+
publish: 'publish',
|
|
44
|
+
userIdAi: 'user_id_ai',
|
|
45
|
+
user_id_ai: 'user_id_ai',
|
|
46
|
+
userAi: 'user_ai',
|
|
47
|
+
user_ai: 'user_ai',
|
|
48
|
+
};
|
|
49
|
+
function flowFilterColumn(field) {
|
|
50
|
+
const column = FLOW_FILTER_COLUMNS[String(field !== null && field !== void 0 ? field : '')];
|
|
51
|
+
if (!column) {
|
|
52
|
+
throw new core_1.PxpError(400, 'Invalid filter field');
|
|
53
|
+
}
|
|
54
|
+
return column;
|
|
55
|
+
}
|
|
56
|
+
exports.flowFilterColumn = flowFilterColumn;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*****************************************************************************
|
|
2
|
+
* Copyright(c) 2026 Qorus Inc
|
|
3
|
+
* All rights reserved
|
|
4
|
+
*****************************************************************************
|
|
5
|
+
* NAME: sessionVendor.ts
|
|
6
|
+
* DEVELOPER: Rensi Arteaga
|
|
7
|
+
* DESCRIPTION: Session vendor for flow templates. Request vendor is ignored for harmony users.
|
|
8
|
+
* REVISIONS:
|
|
9
|
+
* Date Change ID Author Description
|
|
10
|
+
* ----------- ----------- -------------- ------------------------------------
|
|
11
|
+
* 10-Nov-2026 Rensi Arteaga Created
|
|
12
|
+
******************************************************************************/
|
|
13
|
+
export declare function harmonyVendorId(user: any): number | null;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*****************************************************************************
|
|
3
|
+
* Copyright(c) 2026 Qorus Inc
|
|
4
|
+
* All rights reserved
|
|
5
|
+
*****************************************************************************
|
|
6
|
+
* NAME: sessionVendor.ts
|
|
7
|
+
* DEVELOPER: Rensi Arteaga
|
|
8
|
+
* DESCRIPTION: Session vendor for flow templates. Request vendor is ignored for harmony users.
|
|
9
|
+
* REVISIONS:
|
|
10
|
+
* Date Change ID Author Description
|
|
11
|
+
* ----------- ----------- -------------- ------------------------------------
|
|
12
|
+
* 10-Nov-2026 Rensi Arteaga Created
|
|
13
|
+
******************************************************************************/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.harmonyVendorId = void 0;
|
|
16
|
+
function harmonyVendorId(user) {
|
|
17
|
+
var _a;
|
|
18
|
+
const raw = (_a = user === null || user === void 0 ? void 0 : user.harmony) === null || _a === void 0 ? void 0 : _a.vendor_id;
|
|
19
|
+
if (raw == null || raw === '') {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const id = Number(raw);
|
|
23
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return id;
|
|
27
|
+
}
|
|
28
|
+
exports.harmonyVendorId = harmonyVendorId;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*****************************************************************************
|
|
2
|
+
* Copyright(c) 2026 Qorus Inc
|
|
3
|
+
* All rights reserved
|
|
4
|
+
*****************************************************************************
|
|
5
|
+
* NAME: sqlIdent.ts
|
|
6
|
+
* DEVELOPER: Rensi Arteaga
|
|
7
|
+
* DESCRIPTION: Parameterized origin queries. Identifiers are checked, values are bound.
|
|
8
|
+
* REVISIONS:
|
|
9
|
+
* Date Change ID Author Description
|
|
10
|
+
* ----------- ----------- -------------- ------------------------------------
|
|
11
|
+
* 10-Nov-2026 Rensi Arteaga Created
|
|
12
|
+
******************************************************************************/
|
|
13
|
+
export declare function sqlIdent(value: unknown): string;
|
|
14
|
+
export declare function quoteIdent(value: unknown): string;
|
|
15
|
+
export declare function selectByIdent(manager: {
|
|
16
|
+
query: (sql: string, params?: unknown[]) => Promise<any>;
|
|
17
|
+
}, originName: unknown, originKey: unknown, value: unknown): Promise<any[]>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*****************************************************************************
|
|
3
|
+
* Copyright(c) 2026 Qorus Inc
|
|
4
|
+
* All rights reserved
|
|
5
|
+
*****************************************************************************
|
|
6
|
+
* NAME: sqlIdent.ts
|
|
7
|
+
* DEVELOPER: Rensi Arteaga
|
|
8
|
+
* DESCRIPTION: Parameterized origin queries. Identifiers are checked, values are bound.
|
|
9
|
+
* REVISIONS:
|
|
10
|
+
* Date Change ID Author Description
|
|
11
|
+
* ----------- ----------- -------------- ------------------------------------
|
|
12
|
+
* 10-Nov-2026 Rensi Arteaga Created
|
|
13
|
+
******************************************************************************/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.selectByIdent = exports.quoteIdent = exports.sqlIdent = void 0;
|
|
16
|
+
const core_1 = require("@pxp-nd/core");
|
|
17
|
+
const SQL_IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
18
|
+
function sqlIdent(value) {
|
|
19
|
+
const ident = String(value !== null && value !== void 0 ? value : '');
|
|
20
|
+
if (!SQL_IDENT.test(ident)) {
|
|
21
|
+
throw new core_1.PxpError(400, 'Invalid origin');
|
|
22
|
+
}
|
|
23
|
+
return ident;
|
|
24
|
+
}
|
|
25
|
+
exports.sqlIdent = sqlIdent;
|
|
26
|
+
function quoteIdent(value) {
|
|
27
|
+
return '`' + sqlIdent(value) + '`';
|
|
28
|
+
}
|
|
29
|
+
exports.quoteIdent = quoteIdent;
|
|
30
|
+
async function selectByIdent(manager, originName, originKey, value) {
|
|
31
|
+
const sql = `SELECT * FROM ${quoteIdent(originName)} WHERE ${quoteIdent(originKey)} = ?`;
|
|
32
|
+
return manager.query(sql, [value]);
|
|
33
|
+
}
|
|
34
|
+
exports.selectByIdent = selectByIdent;
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pxp-nd/flow",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.32",
|
|
4
4
|
"description": "Workflow module for pxp-nd framework",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"prepublish": "tsc",
|
|
8
8
|
"scripts": {
|
|
9
|
-
"test": "
|
|
9
|
+
"test": "jest --runInBand",
|
|
10
10
|
"build": "rimraf ./dist && tsc",
|
|
11
11
|
"dev:publish": "yalc publish",
|
|
12
12
|
"prod:publish": "npm publish",
|
|
@@ -24,6 +24,13 @@
|
|
|
24
24
|
],
|
|
25
25
|
"author": "Kplian",
|
|
26
26
|
"license": "ISC",
|
|
27
|
+
"jest": {
|
|
28
|
+
"preset": "ts-jest",
|
|
29
|
+
"testEnvironment": "node",
|
|
30
|
+
"roots": ["<rootDir>/test"],
|
|
31
|
+
"testTimeout": 30000,
|
|
32
|
+
"globals": { "ts-jest": { "tsconfig": "<rootDir>/test/tsconfig.json" } }
|
|
33
|
+
},
|
|
27
34
|
"bugs": {
|
|
28
35
|
"url": "https://github.com/kplian/flow-nd/issues"
|
|
29
36
|
},
|
|
@@ -31,13 +38,16 @@
|
|
|
31
38
|
"devDependencies": {
|
|
32
39
|
"@types/lodash": "^4.14.172",
|
|
33
40
|
"@types/node": "^16.4.13",
|
|
41
|
+
"@types/jest": "^27.5.2",
|
|
42
|
+
"jest": "^27.5.1",
|
|
43
|
+
"ts-jest": "^27.1.5",
|
|
34
44
|
"rimraf": "^3.0.2",
|
|
35
45
|
"tsc-watch": "^4.6.2",
|
|
36
46
|
"typescript": "^4.9.5"
|
|
37
47
|
},
|
|
38
48
|
"dependencies": {
|
|
39
49
|
"@pxp-nd/common": "^1.1.4",
|
|
40
|
-
"@pxp-nd/core": "^1.2.
|
|
50
|
+
"@pxp-nd/core": "^1.2.96",
|
|
41
51
|
"axios": "^0.21.1",
|
|
42
52
|
"lodash": "^4.17.21",
|
|
43
53
|
"typeorm": "^0.2.36"
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { getManager } from 'typeorm';
|
|
3
|
+
import { PxpError } from '@pxp-nd/core';
|
|
4
|
+
import NodeModel from '../src/entity/Node';
|
|
5
|
+
import NodeInstanceModel from '../src/entity/NodeInstance';
|
|
6
|
+
import Event from '../src/entity/Event';
|
|
7
|
+
import Flow from '../src/entity/Flow';
|
|
8
|
+
import NodeController from '../src/controllers/Node';
|
|
9
|
+
import FlowInstanceController from '../src/controllers/FlowInstance';
|
|
10
|
+
import NodeInstanceController from '../src/controllers/NodeInstance';
|
|
11
|
+
import { flowFilterColumn } from '../src/helpers/flowFilter';
|
|
12
|
+
|
|
13
|
+
jest.mock('typeorm', () => {
|
|
14
|
+
const actual = jest.requireActual('typeorm');
|
|
15
|
+
return {
|
|
16
|
+
...actual,
|
|
17
|
+
getManager: jest.fn(),
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const pxpConfig = { apiPrefix: '/api', defaultDbSettings: 'Orm' as const, middlewares: [] as any[] };
|
|
22
|
+
|
|
23
|
+
function queryManager() {
|
|
24
|
+
const query = jest.fn().mockResolvedValue([{ id: 1 }]);
|
|
25
|
+
const save = jest.fn().mockResolvedValue({});
|
|
26
|
+
(getManager as unknown as jest.Mock).mockReturnValue({ query, save });
|
|
27
|
+
return { query, save };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('select by identifier', () => {
|
|
31
|
+
it('binds a stored value and rejects a bad origin before querying', async () => {
|
|
32
|
+
const ctrl = new NodeController('flow-nd', NodeModel, pxpConfig);
|
|
33
|
+
const query = jest.fn();
|
|
34
|
+
await expect(
|
|
35
|
+
ctrl.getNodeData({ action: { originName: 'bad name', originKey: 'id' } }, 1, { query } as any)
|
|
36
|
+
).rejects.toBeInstanceOf(PxpError);
|
|
37
|
+
expect(query).not.toHaveBeenCalled();
|
|
38
|
+
|
|
39
|
+
const ok = jest.fn().mockResolvedValue([{ id: 7 }]);
|
|
40
|
+
const raw = await ctrl.getNodeData(
|
|
41
|
+
{ action: { originName: 'people', originKey: 'person_id' } },
|
|
42
|
+
'1 UNION SELECT 1',
|
|
43
|
+
{ query: ok } as any,
|
|
44
|
+
);
|
|
45
|
+
expect(ok).toHaveBeenCalledWith(
|
|
46
|
+
'SELECT * FROM `people` WHERE `person_id` = ?',
|
|
47
|
+
['1 UNION SELECT 1'],
|
|
48
|
+
);
|
|
49
|
+
expect(raw).toBe(JSON.stringify({ id: 7 }));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('executeActionJson uses the action row, not the request identifiers', async () => {
|
|
53
|
+
const { query } = queryManager();
|
|
54
|
+
const ctrl = new NodeController('flow-nd', NodeModel, pxpConfig);
|
|
55
|
+
jest.spyOn(NodeModel, 'findOne').mockResolvedValue({
|
|
56
|
+
action: { originName: 'people', originKey: 'person_id' },
|
|
57
|
+
} as any);
|
|
58
|
+
jest.spyOn(ctrl, 'getParameterizedNode').mockResolvedValue({
|
|
59
|
+
mergeValues: { payload: '1 UNION SELECT 1' },
|
|
60
|
+
} as any);
|
|
61
|
+
|
|
62
|
+
await ctrl.executeActionJson(
|
|
63
|
+
{ nodeId: 4, fromValues: 'payload', originName: 'other_table', originKey: 'other_col' },
|
|
64
|
+
{} as any,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
expect(query).toHaveBeenCalledWith(
|
|
68
|
+
'SELECT * FROM `people` WHERE `person_id` = ?',
|
|
69
|
+
['1 UNION SELECT 1'],
|
|
70
|
+
);
|
|
71
|
+
expect(query.mock.calls[0][0]).not.toContain('UNION');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('executeActionJson rejects an action origin that is not an identifier', async () => {
|
|
75
|
+
const { query } = queryManager();
|
|
76
|
+
const ctrl = new NodeController('flow-nd', NodeModel, pxpConfig);
|
|
77
|
+
jest.spyOn(NodeModel, 'findOne').mockResolvedValue({
|
|
78
|
+
action: { originName: 'people;drop', originKey: 'id' },
|
|
79
|
+
} as any);
|
|
80
|
+
jest.spyOn(ctrl, 'getParameterizedNode').mockResolvedValue({ mergeValues: { payload: '1' } } as any);
|
|
81
|
+
|
|
82
|
+
await expect(
|
|
83
|
+
ctrl.executeActionJson({ nodeId: 4, fromValues: 'payload' }, {} as any)
|
|
84
|
+
).rejects.toBeInstanceOf(PxpError);
|
|
85
|
+
expect(query).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('event and node instance queries', () => {
|
|
90
|
+
it('eventProcesss binds dataId', async () => {
|
|
91
|
+
const { query } = queryManager();
|
|
92
|
+
jest.spyOn(Event, 'find').mockResolvedValue([
|
|
93
|
+
{ eventId: 1, actionId: 2, dataId: '1 UNION SELECT 1', status: 'pending' },
|
|
94
|
+
] as any);
|
|
95
|
+
jest.spyOn(NodeModel, 'find').mockResolvedValue([
|
|
96
|
+
{ flowId: 9, action: { originName: 'people', originKey: 'person_id' } },
|
|
97
|
+
] as any);
|
|
98
|
+
jest.spyOn(Flow, 'findOne').mockResolvedValue(null as any);
|
|
99
|
+
const ctrl = new FlowInstanceController('flow-nd', null, pxpConfig);
|
|
100
|
+
await ctrl.eventProcesss({} as any);
|
|
101
|
+
expect(query).toHaveBeenCalledWith(
|
|
102
|
+
'SELECT * FROM `people` WHERE `person_id` = ?',
|
|
103
|
+
['1 UNION SELECT 1'],
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('RecursiveInstance binds dataId and uses the action identifiers', async () => {
|
|
108
|
+
const { query } = queryManager();
|
|
109
|
+
const ctrl = new NodeInstanceController('flow-nd', NodeInstanceModel, pxpConfig);
|
|
110
|
+
await ctrl.RecursiveInstance({
|
|
111
|
+
node: {
|
|
112
|
+
nodeId: 3,
|
|
113
|
+
actionConfigJson: '',
|
|
114
|
+
action: {
|
|
115
|
+
originName: 'people',
|
|
116
|
+
originKey: 'person_id',
|
|
117
|
+
configJsonTemplate: '',
|
|
118
|
+
actionType: { isDelay: 'N', controller: '' },
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
flowInstance: {
|
|
122
|
+
flowInstanceId: 8,
|
|
123
|
+
originName: 'evil',
|
|
124
|
+
originKey: 'evil',
|
|
125
|
+
dataId: '1 UNION SELECT 1',
|
|
126
|
+
},
|
|
127
|
+
}, { save: jest.fn() } as any);
|
|
128
|
+
expect(query.mock.calls[0][0]).toBe('SELECT * FROM `people` WHERE `person_id` = ?');
|
|
129
|
+
expect(query.mock.calls[0][1]).toEqual(['1 UNION SELECT 1']);
|
|
130
|
+
expect(query.mock.calls[0][0]).not.toContain('evil');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('getParameterizedNode vendor', () => {
|
|
135
|
+
it('fills the template with the session vendor', async () => {
|
|
136
|
+
const ctrl = new NodeController('flow-nd', NodeModel, pxpConfig);
|
|
137
|
+
ctrl.user = { harmony: { vendor_id: 7 } };
|
|
138
|
+
const qb = {
|
|
139
|
+
innerJoin: jest.fn().mockReturnThis(),
|
|
140
|
+
where: jest.fn().mockReturnThis(),
|
|
141
|
+
getMany: jest.fn().mockResolvedValue([]),
|
|
142
|
+
};
|
|
143
|
+
jest.spyOn(NodeModel, 'findOne')
|
|
144
|
+
.mockResolvedValueOnce({
|
|
145
|
+
flowId: 1,
|
|
146
|
+
actionConfigJson: null,
|
|
147
|
+
action: {
|
|
148
|
+
schemaJson: '{"v":{{vendorId}}}',
|
|
149
|
+
configJsonTemplate: null,
|
|
150
|
+
actionType: { schemaJson: null },
|
|
151
|
+
},
|
|
152
|
+
} as any)
|
|
153
|
+
.mockResolvedValueOnce({ action: { originName: 'people' } } as any);
|
|
154
|
+
const result: any = await ctrl.getParameterizedNode(
|
|
155
|
+
{ nodeId: 1, substitutionsSchemaJson: { vendorId: 99 } },
|
|
156
|
+
{ createQueryBuilder: () => qb } as any,
|
|
157
|
+
);
|
|
158
|
+
expect(result.schemaJsonObject.v).toBe(7);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('flowFilterColumn', () => {
|
|
163
|
+
it('maps a known field and rejects anything else', () => {
|
|
164
|
+
expect(flowFilterColumn('name')).toBe('name');
|
|
165
|
+
expect(flowFilterColumn('createdAt')).toBe('created_at');
|
|
166
|
+
expect(() => flowFilterColumn('name; drop table twf_flow')).toThrow(PxpError);
|
|
167
|
+
});
|
|
168
|
+
});
|