@pxp-nd/flow 1.1.30 → 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/Action.d.ts +1 -1
- package/dist/controllers/Action.js +8 -2
- package/dist/controllers/Flow.d.ts +1 -0
- package/dist/controllers/Flow.js +52 -23
- package/dist/controllers/FlowInstance.js +2 -2
- package/dist/controllers/Node.d.ts +1 -0
- package/dist/controllers/Node.js +27 -12
- package/dist/controllers/NodeInstance.js +6 -4
- package/dist/helpers/FlowNdApiDocs.d.ts +19 -0
- package/dist/helpers/FlowNdApiDocs.js +299 -0
- 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
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* @author Favio Figueroa
|
|
10
10
|
*
|
|
11
11
|
* Created at : 2021-07-08 12:55:38
|
|
12
|
-
* Last modified :
|
|
12
|
+
* Last modified : 2026-11-06 — Rensi Arteaga — @ApiDoc for OpenAPI (spec 04)
|
|
13
13
|
*/
|
|
14
14
|
import { EntityManager } from 'typeorm';
|
|
15
15
|
import { Controller } from '@pxp-nd/core';
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* @author Favio Figueroa
|
|
11
11
|
*
|
|
12
12
|
* Created at : 2021-07-08 12:55:38
|
|
13
|
-
* Last modified :
|
|
13
|
+
* Last modified : 2026-11-06 — Rensi Arteaga — @ApiDoc for OpenAPI (spec 04)
|
|
14
14
|
*/
|
|
15
15
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
16
16
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
@@ -30,9 +30,13 @@ const core_1 = require("@pxp-nd/core");
|
|
|
30
30
|
const Action_1 = __importDefault(require("../entity/Action"));
|
|
31
31
|
const FieldMap_1 = __importDefault(require("../entity/FieldMap"));
|
|
32
32
|
const OriginName_1 = __importDefault(require("../entity/OriginName"));
|
|
33
|
+
const FlowNdApiDocs_1 = require("../helpers/FlowNdApiDocs");
|
|
33
34
|
let Action = class Action extends core_1.Controller {
|
|
34
35
|
async getFieldMapByAction(params, manager) {
|
|
35
36
|
const { originName } = params;
|
|
37
|
+
if (originName == null || String(originName).trim() === '') {
|
|
38
|
+
throw new core_1.PxpError(400, 'originName is required');
|
|
39
|
+
}
|
|
36
40
|
const fieldMapData = await (0, core_1.__)(manager.createQueryBuilder(FieldMap_1.default, 'fm')
|
|
37
41
|
.innerJoin(OriginName_1.default, 'on', 'on.originNameId = fm.originNameId')
|
|
38
42
|
.where("on.name = :n ", { n: originName })
|
|
@@ -61,8 +65,9 @@ let Action = class Action extends core_1.Controller {
|
|
|
61
65
|
__decorate([
|
|
62
66
|
(0, core_1.Get)(),
|
|
63
67
|
(0, core_1.DbSettings)('Orm'),
|
|
64
|
-
(0, core_1.ReadOnly)(
|
|
68
|
+
(0, core_1.ReadOnly)(true),
|
|
65
69
|
(0, core_1.Log)(true),
|
|
70
|
+
(0, core_1.ApiDoc)(FlowNdApiDocs_1.actionGetFieldMapByActionDoc),
|
|
66
71
|
__metadata("design:type", Function),
|
|
67
72
|
__metadata("design:paramtypes", [Object, typeorm_1.EntityManager]),
|
|
68
73
|
__metadata("design:returntype", Promise)
|
|
@@ -72,6 +77,7 @@ __decorate([
|
|
|
72
77
|
(0, core_1.DbSettings)('Orm'),
|
|
73
78
|
(0, core_1.ReadOnly)(true),
|
|
74
79
|
(0, core_1.Log)(true),
|
|
80
|
+
(0, core_1.ApiDoc)(FlowNdApiDocs_1.actionGetActionsDoc),
|
|
75
81
|
__metadata("design:type", Function),
|
|
76
82
|
__metadata("design:paramtypes", [Object, typeorm_1.EntityManager]),
|
|
77
83
|
__metadata("design:returntype", Promise)
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* 28-Sep-2023 SP06OCT23 Mercedes Zambrana Add Validation when change off status (validateFlow, deleteflow, saveFlow, saveflowName)
|
|
20
20
|
* 07-Feb-2025 8353064698 Mercedes Zambrana Add validation for opt in page when flow si funnel change
|
|
21
21
|
* 18-Feb-2025 8501163144 Mercedes Zambrana Add publish filter
|
|
22
|
+
* 06-Nov-2026 Rensi Arteaga @ApiDoc for OpenAPI (spec 04)
|
|
22
23
|
* ******************************************************************************
|
|
23
24
|
*/
|
|
24
25
|
import { EntityManager } from 'typeorm';
|
package/dist/controllers/Flow.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* 28-Sep-2023 SP06OCT23 Mercedes Zambrana Add Validation when change off status (validateFlow, deleteflow, saveFlow, saveflowName)
|
|
21
21
|
* 07-Feb-2025 8353064698 Mercedes Zambrana Add validation for opt in page when flow si funnel change
|
|
22
22
|
* 18-Feb-2025 8501163144 Mercedes Zambrana Add publish filter
|
|
23
|
+
* 06-Nov-2026 Rensi Arteaga @ApiDoc for OpenAPI (spec 04)
|
|
23
24
|
* ******************************************************************************
|
|
24
25
|
*/
|
|
25
26
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
@@ -67,10 +68,16 @@ const NodeConnection_1 = __importDefault(require("../entity/NodeConnection"));
|
|
|
67
68
|
const Flow_1 = __importDefault(require("../entity/Flow"));
|
|
68
69
|
const FlowInstance_1 = __importDefault(require("../entity/FlowInstance"));
|
|
69
70
|
const Node_2 = __importDefault(require("./Node"));
|
|
71
|
+
const flowFilter_1 = require("../helpers/flowFilter");
|
|
70
72
|
const axios_1 = __importDefault(require("axios"));
|
|
71
73
|
//import {configFacebookStrategy} from "./passport-facebook";
|
|
74
|
+
const FlowNdApiDocs_1 = require("../helpers/FlowNdApiDocs");
|
|
72
75
|
let Flow = class Flow extends core_1.Controller {
|
|
73
76
|
async get(params) {
|
|
77
|
+
const fid = Number(params.flowId);
|
|
78
|
+
if (!Number.isInteger(fid) || fid < 1) {
|
|
79
|
+
throw new core_1.PxpError(400, 'flowId is required and must be a positive integer');
|
|
80
|
+
}
|
|
74
81
|
const nodes = await Node_1.default.find({ flowId: params.flowId });
|
|
75
82
|
const restNodes = [];
|
|
76
83
|
for (let node of nodes) {
|
|
@@ -186,14 +193,10 @@ let Flow = class Flow extends core_1.Controller {
|
|
|
186
193
|
dataFlow.modifiedBy = this.user.username;
|
|
187
194
|
const updFlow = await (0, core_1.__)(manager.save(dataFlow));
|
|
188
195
|
if (updFlow) {
|
|
189
|
-
|
|
190
|
-
is_active = 'N' WHERE flow_id = ${updFlow.flowId}`;
|
|
191
|
-
await manager.query(flowInstance);
|
|
196
|
+
await manager.query(`UPDATE twf_flow_instance SET is_active = 'N' WHERE flow_id = ?`, [updFlow.flowId]);
|
|
192
197
|
const nodesToInact = await manager.find(Node_1.default, { flowId: updFlow.flowId, isActive: true });
|
|
193
198
|
if (nodesToInact.length > 0) {
|
|
194
|
-
|
|
195
|
-
is_active = 'N' WHERE flow_id = ${updFlow.flowId}`;
|
|
196
|
-
await manager.query(flowNode);
|
|
199
|
+
await manager.query(`UPDATE twf_node SET is_active = 'N' WHERE flow_id = ?`, [updFlow.flowId]);
|
|
197
200
|
const nodesToInact = await manager.find(Node_1.default, { flowId: updFlow.flowId });
|
|
198
201
|
const originalNodeIds = nodesToInact.map((node) => node.nodeId);
|
|
199
202
|
const connectionsToInact = await manager.find(NodeConnection_1.default, {
|
|
@@ -201,9 +204,7 @@ let Flow = class Flow extends core_1.Controller {
|
|
|
201
204
|
});
|
|
202
205
|
const originalNodeCIds = connectionsToInact.map((node) => node.nodeConnectionId);
|
|
203
206
|
if (originalNodeCIds.length > 0) {
|
|
204
|
-
|
|
205
|
-
is_active = 'N' WHERE node_connection_id in ( ${originalNodeCIds})`;
|
|
206
|
-
await manager.query(flowNodeC);
|
|
207
|
+
await manager.query(`UPDATE twf_node_connection SET is_active = 'N' WHERE node_connection_id IN (?)`, [originalNodeCIds]);
|
|
207
208
|
}
|
|
208
209
|
}
|
|
209
210
|
return { success: true };
|
|
@@ -322,9 +323,9 @@ let Flow = class Flow extends core_1.Controller {
|
|
|
322
323
|
});
|
|
323
324
|
if (connectionsToDel) {
|
|
324
325
|
const originalNodeCIds = connectionsToDel.map((node) => node.nodeConnectionId);
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
326
|
+
if (originalNodeCIds.length > 0) {
|
|
327
|
+
await manager.query(`DELETE FROM twf_node_connection WHERE node_connection_id IN (?)`, [originalNodeCIds]);
|
|
328
|
+
}
|
|
328
329
|
}
|
|
329
330
|
}
|
|
330
331
|
}
|
|
@@ -343,9 +344,9 @@ let Flow = class Flow extends core_1.Controller {
|
|
|
343
344
|
allowModify = true;
|
|
344
345
|
//If there are some nodes in node_connections after delete all records sent, to inactive node
|
|
345
346
|
let connectNoDel = await (0, typeorm_1.getManager)().query(`SELECT n.node_id
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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]);
|
|
349
350
|
const currentNodesIds = connectNoDel.map((record) => record.node_id);
|
|
350
351
|
// Get nodes to remove
|
|
351
352
|
const nodesToRemove = currentNodesIds.filter((nodeId) => !taskIds.includes(nodeId.toString()));
|
|
@@ -353,10 +354,7 @@ let Flow = class Flow extends core_1.Controller {
|
|
|
353
354
|
if (nodesToRemove.length > 0) {
|
|
354
355
|
const delNodeConnect = await manager.delete(NodeConnection_1.default, { nodeIdChild: nodesToRemove[0] });
|
|
355
356
|
if (delNodeConnect) {
|
|
356
|
-
|
|
357
|
-
SET is_active = 'N'
|
|
358
|
-
WHERE node_id = ${nodesToRemove[0]}`;
|
|
359
|
-
await manager.query(dNode);
|
|
357
|
+
await manager.query(`UPDATE twf_node SET is_active = 'N' WHERE node_id = ?`, [nodesToRemove[0]]);
|
|
360
358
|
}
|
|
361
359
|
}
|
|
362
360
|
if (flowIdDel.action.actionType.name === 'CHAINED FLOW') {
|
|
@@ -477,6 +475,10 @@ let Flow = class Flow extends core_1.Controller {
|
|
|
477
475
|
}
|
|
478
476
|
async getFlowRender(params) {
|
|
479
477
|
var _a;
|
|
478
|
+
const fid = Number(params.flowId);
|
|
479
|
+
if (!Number.isInteger(fid) || fid < 1) {
|
|
480
|
+
throw new core_1.PxpError(400, 'flowId is required and must be a positive integer');
|
|
481
|
+
}
|
|
480
482
|
const flow = await (0, typeorm_1.getManager)().findOne(Flow_1.default, params.flowId);
|
|
481
483
|
const totalNodes = await (0, typeorm_1.getManager)()
|
|
482
484
|
.createQueryBuilder(Node_1.default, "n")
|
|
@@ -782,6 +784,15 @@ let Flow = class Flow extends core_1.Controller {
|
|
|
782
784
|
const filter = (params === null || params === void 0 ? void 0 : params.filter) ? JSON.parse(params === null || params === void 0 ? void 0 : params.filter) : null;
|
|
783
785
|
const filters = (filter === null || filter === void 0 ? void 0 : filter.items) || [];
|
|
784
786
|
const logicOperator = ((_a = filter === null || filter === void 0 ? void 0 : filter.logicOperator) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || 'AND';
|
|
787
|
+
const allowedSort = ['flowId', 'name', 'createdAt', 'modifiedAt'];
|
|
788
|
+
const allowedDir = ['ASC', 'DESC'];
|
|
789
|
+
const sortCol = params.sort != null && params.sort !== '' ? String(params.sort) : 'flowId';
|
|
790
|
+
const dirVal = params.dir != null && params.dir !== ''
|
|
791
|
+
? String(params.dir).toUpperCase()
|
|
792
|
+
: 'ASC';
|
|
793
|
+
if (!allowedSort.includes(sortCol) || !allowedDir.includes(dirVal)) {
|
|
794
|
+
throw new core_1.PxpError(400, 'sort and dir must be allowed values (whitelist for SQL orderBy)');
|
|
795
|
+
}
|
|
785
796
|
const queryBuilder = await (0, typeorm_1.getManager)()
|
|
786
797
|
.createQueryBuilder()
|
|
787
798
|
.select([
|
|
@@ -823,7 +834,7 @@ let Flow = class Flow extends core_1.Controller {
|
|
|
823
834
|
const applyDynamicFilters = (queryBuilder, filters, logicOperator) => {
|
|
824
835
|
filters.forEach((filter, index) => {
|
|
825
836
|
const { field, operator, value } = filter;
|
|
826
|
-
const queryField = `f.${field}`;
|
|
837
|
+
const queryField = `f.${(0, flowFilter_1.flowFilterColumn)(field)}`;
|
|
827
838
|
const parameterName = `filterValue${index}`;
|
|
828
839
|
let condition;
|
|
829
840
|
switch (operator) {
|
|
@@ -888,7 +899,7 @@ let Flow = class Flow extends core_1.Controller {
|
|
|
888
899
|
// Sorting and pagination
|
|
889
900
|
//queryBuilder.orderBy(`f.${params.sort}`, params.dir).skip(params.start).take(params.limit);
|
|
890
901
|
// Sorting and pagination
|
|
891
|
-
queryBuilder.orderBy(`f.${
|
|
902
|
+
queryBuilder.orderBy(`f.${sortCol}`, dirVal);
|
|
892
903
|
// Agrega manualmente la cláusula LIMIT y OFFSET
|
|
893
904
|
queryBuilder.limit(params.limit).offset(params.start);
|
|
894
905
|
const data = await queryBuilder.getRawMany();
|
|
@@ -929,6 +940,15 @@ let Flow = class Flow extends core_1.Controller {
|
|
|
929
940
|
const filters = (filter === null || filter === void 0 ? void 0 : filter.items) || [];
|
|
930
941
|
const logicOperator = ((_a = filter === null || filter === void 0 ? void 0 : filter.logicOperator) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || "AND";
|
|
931
942
|
const publish = params._publish;
|
|
943
|
+
const allowedSort = ["flowId", "name", "createdAt", "modifiedAt"];
|
|
944
|
+
const allowedDir = ["ASC", "DESC"];
|
|
945
|
+
const sortCol = params.sort != null && params.sort !== "" ? String(params.sort) : "flowId";
|
|
946
|
+
const dirVal = params.dir != null && params.dir !== ""
|
|
947
|
+
? String(params.dir).toUpperCase()
|
|
948
|
+
: "ASC";
|
|
949
|
+
if (!allowedSort.includes(sortCol) || !allowedDir.includes(dirVal)) {
|
|
950
|
+
throw new core_1.PxpError(400, "sort and dir must be allowed values (whitelist for SQL orderBy)");
|
|
951
|
+
}
|
|
932
952
|
const queryBuilder = await (0, typeorm_1.getManager)()
|
|
933
953
|
.createQueryBuilder()
|
|
934
954
|
.select([
|
|
@@ -978,7 +998,7 @@ let Flow = class Flow extends core_1.Controller {
|
|
|
978
998
|
const applyDynamicFilters = (queryBuilder, filters, logicOperator) => {
|
|
979
999
|
filters.forEach((filter, index) => {
|
|
980
1000
|
const { field, operator, value } = filter;
|
|
981
|
-
const queryField = `f.${field}`;
|
|
1001
|
+
const queryField = `f.${(0, flowFilter_1.flowFilterColumn)(field)}`;
|
|
982
1002
|
const parameterName = `filterValue${index}`;
|
|
983
1003
|
let condition;
|
|
984
1004
|
switch (operator) {
|
|
@@ -1040,7 +1060,7 @@ let Flow = class Flow extends core_1.Controller {
|
|
|
1040
1060
|
"f.status",
|
|
1041
1061
|
]);
|
|
1042
1062
|
// Ordenamiento y paginación
|
|
1043
|
-
queryBuilder.orderBy(`f.${
|
|
1063
|
+
queryBuilder.orderBy(`f.${sortCol}`, dirVal);
|
|
1044
1064
|
queryBuilder.limit(params.limit || 50).offset(params.start || 0);
|
|
1045
1065
|
// Obtener los datos
|
|
1046
1066
|
const data = await queryBuilder.getRawMany();
|
|
@@ -1117,6 +1137,7 @@ __decorate([
|
|
|
1117
1137
|
(0, core_1.DbSettings)('Orm'),
|
|
1118
1138
|
(0, core_1.ReadOnly)(true),
|
|
1119
1139
|
(0, core_1.Log)(true),
|
|
1140
|
+
(0, core_1.ApiDoc)(FlowNdApiDocs_1.flowGetDoc),
|
|
1120
1141
|
__metadata("design:type", Function),
|
|
1121
1142
|
__metadata("design:paramtypes", [Object]),
|
|
1122
1143
|
__metadata("design:returntype", Promise)
|
|
@@ -1135,6 +1156,7 @@ __decorate([
|
|
|
1135
1156
|
(0, core_1.DbSettings)('Orm'),
|
|
1136
1157
|
(0, core_1.ReadOnly)(false),
|
|
1137
1158
|
(0, core_1.Log)(true),
|
|
1159
|
+
(0, core_1.ApiDoc)(FlowNdApiDocs_1.flowDeleteFlowDoc),
|
|
1138
1160
|
__metadata("design:type", Function),
|
|
1139
1161
|
__metadata("design:paramtypes", [Object, typeorm_1.EntityManager]),
|
|
1140
1162
|
__metadata("design:returntype", Promise)
|
|
@@ -1144,6 +1166,7 @@ __decorate([
|
|
|
1144
1166
|
(0, core_1.DbSettings)('Orm'),
|
|
1145
1167
|
(0, core_1.ReadOnly)(false),
|
|
1146
1168
|
(0, core_1.Log)(true),
|
|
1169
|
+
(0, core_1.ApiDoc)(FlowNdApiDocs_1.flowSaveFlowNameDoc),
|
|
1147
1170
|
__metadata("design:type", Function),
|
|
1148
1171
|
__metadata("design:paramtypes", [Object, typeorm_1.EntityManager]),
|
|
1149
1172
|
__metadata("design:returntype", Promise)
|
|
@@ -1162,6 +1185,7 @@ __decorate([
|
|
|
1162
1185
|
(0, core_1.DbSettings)('Orm'),
|
|
1163
1186
|
(0, core_1.ReadOnly)(false),
|
|
1164
1187
|
(0, core_1.Log)(true),
|
|
1188
|
+
(0, core_1.ApiDoc)(FlowNdApiDocs_1.flowSaveFlowDoc),
|
|
1165
1189
|
__metadata("design:type", Function),
|
|
1166
1190
|
__metadata("design:paramtypes", [Object, typeorm_1.EntityManager]),
|
|
1167
1191
|
__metadata("design:returntype", Promise)
|
|
@@ -1172,6 +1196,7 @@ __decorate([
|
|
|
1172
1196
|
(0, core_1.DbSettings)('Orm'),
|
|
1173
1197
|
(0, core_1.ReadOnly)(true),
|
|
1174
1198
|
(0, core_1.Log)(true),
|
|
1199
|
+
(0, core_1.ApiDoc)(FlowNdApiDocs_1.flowGetFlowRenderDoc),
|
|
1175
1200
|
__metadata("design:type", Function),
|
|
1176
1201
|
__metadata("design:paramtypes", [Object]),
|
|
1177
1202
|
__metadata("design:returntype", Promise)
|
|
@@ -1181,6 +1206,7 @@ __decorate([
|
|
|
1181
1206
|
(0, core_1.DbSettings)('Orm'),
|
|
1182
1207
|
(0, core_1.ReadOnly)(false),
|
|
1183
1208
|
(0, core_1.Log)(true),
|
|
1209
|
+
(0, core_1.ApiDoc)(FlowNdApiDocs_1.flowInsertEventFlowDoc),
|
|
1184
1210
|
__metadata("design:type", Function),
|
|
1185
1211
|
__metadata("design:paramtypes", [Object, typeorm_1.EntityManager]),
|
|
1186
1212
|
__metadata("design:returntype", Promise)
|
|
@@ -1199,6 +1225,7 @@ __decorate([
|
|
|
1199
1225
|
(0, core_1.DbSettings)('Orm'),
|
|
1200
1226
|
(0, core_1.ReadOnly)(false),
|
|
1201
1227
|
(0, core_1.Log)(true),
|
|
1228
|
+
(0, core_1.ApiDoc)(FlowNdApiDocs_1.flowChangeStatusFlowDoc),
|
|
1202
1229
|
__metadata("design:type", Function),
|
|
1203
1230
|
__metadata("design:paramtypes", [Object, typeorm_1.EntityManager]),
|
|
1204
1231
|
__metadata("design:returntype", Promise)
|
|
@@ -1217,6 +1244,7 @@ __decorate([
|
|
|
1217
1244
|
(0, core_1.DbSettings)("Orm"),
|
|
1218
1245
|
(0, core_1.ReadOnly)(true),
|
|
1219
1246
|
(0, core_1.Log)(true),
|
|
1247
|
+
(0, core_1.ApiDoc)(FlowNdApiDocs_1.flowListTemplatesDoc),
|
|
1220
1248
|
__metadata("design:type", Function),
|
|
1221
1249
|
__metadata("design:paramtypes", [Object]),
|
|
1222
1250
|
__metadata("design:returntype", Promise)
|
|
@@ -1231,6 +1259,7 @@ __decorate([
|
|
|
1231
1259
|
__metadata("design:returntype", Promise)
|
|
1232
1260
|
], Flow.prototype, "changePublishTemplate", null);
|
|
1233
1261
|
Flow = __decorate([
|
|
1262
|
+
(0, core_1.ApiDocFor)('add', FlowNdApiDocs_1.flowAddDoc),
|
|
1234
1263
|
(0, core_1.Model)('flow-nd/Flow')
|
|
1235
1264
|
], Flow);
|
|
1236
1265
|
exports.default = Flow;
|
|
@@ -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))) {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* 04-Sep-2023 SP08SEP23 Rensi Arteaga add modifiedAt for flows
|
|
11
11
|
* 29-Sep-2023 SP06OCT23 Mercedes Zambrana Add validation when flow is on
|
|
12
12
|
* 19-Jan-2025 8201489097 Favio Figueroa Add Controller Validation
|
|
13
|
+
* 06-Nov-2026 Rensi Arteaga @ApiDoc for OpenAPI (spec 04)
|
|
13
14
|
* ******************************************************************************
|
|
14
15
|
*/
|
|
15
16
|
import { EntityManager } from 'typeorm';
|
package/dist/controllers/Node.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* 04-Sep-2023 SP08SEP23 Rensi Arteaga add modifiedAt for flows
|
|
12
12
|
* 29-Sep-2023 SP06OCT23 Mercedes Zambrana Add validation when flow is on
|
|
13
13
|
* 19-Jan-2025 8201489097 Favio Figueroa Add Controller Validation
|
|
14
|
+
* 06-Nov-2026 Rensi Arteaga @ApiDoc for OpenAPI (spec 04)
|
|
14
15
|
* ******************************************************************************
|
|
15
16
|
*/
|
|
16
17
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
@@ -35,11 +36,13 @@ const FieldMap_1 = __importDefault(require("../entity/FieldMap"));
|
|
|
35
36
|
const OriginName_1 = __importDefault(require("../entity/OriginName"));
|
|
36
37
|
const axios_1 = __importDefault(require("axios"));
|
|
37
38
|
const Flow_1 = __importDefault(require("../entity/Flow"));
|
|
39
|
+
const FlowNdApiDocs_1 = require("../helpers/FlowNdApiDocs");
|
|
40
|
+
const sqlIdent_1 = require("../helpers/sqlIdent");
|
|
41
|
+
const sessionVendor_1 = require("../helpers/sessionVendor");
|
|
38
42
|
let Node = class Node extends core_1.Controller {
|
|
39
43
|
async getNodeData(node, dataId, manager) {
|
|
40
44
|
const { action: { originName, originKey } } = node;
|
|
41
|
-
const
|
|
42
|
-
const resExecuteView = await (0, core_1.__)(manager.query(executeView));
|
|
45
|
+
const resExecuteView = await (0, core_1.__)((0, sqlIdent_1.selectByIdent)(manager, originName, originKey, dataId));
|
|
43
46
|
return JSON.stringify(resExecuteView[0]);
|
|
44
47
|
}
|
|
45
48
|
async add(params, manager) {
|
|
@@ -113,7 +116,18 @@ let Node = class Node extends core_1.Controller {
|
|
|
113
116
|
}
|
|
114
117
|
async getParameterizedNode(params, manager) {
|
|
115
118
|
lodash_1.default.templateSettings.interpolate = /{{([\s\S]+?)}}/g;
|
|
116
|
-
const
|
|
119
|
+
const nid = Number(params.nodeId);
|
|
120
|
+
if (!Number.isInteger(nid) || nid < 1) {
|
|
121
|
+
throw new core_1.PxpError(400, 'nodeId is required and must be a positive integer');
|
|
122
|
+
}
|
|
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;
|
|
117
131
|
const tNodeData = (0, core_1.__)(Node_1.default.findOne(nodeId));
|
|
118
132
|
const nodeData = await tNodeData;
|
|
119
133
|
const tNodeOrigin = await (0, core_1.__)(Node_1.default.findOne({
|
|
@@ -255,17 +269,15 @@ let Node = class Node extends core_1.Controller {
|
|
|
255
269
|
};
|
|
256
270
|
}
|
|
257
271
|
async executeActionJson(params, manager) {
|
|
258
|
-
const { nodeId,
|
|
259
|
-
|
|
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
|
+
}
|
|
260
277
|
const getParameterizedNodeData = await (0, core_1.__)(this.getParameterizedNode({ nodeId: nodeId }, manager));
|
|
261
278
|
const { mergeValues } = getParameterizedNodeData;
|
|
262
|
-
const
|
|
263
|
-
const
|
|
264
|
-
const originValue = resValueObject[fromValues];
|
|
265
|
-
const executeViewString = `select * from ${originName} where ${originKey} = ${originValue}`;
|
|
266
|
-
const resExecuteView = await (0, typeorm_1.getManager)().query(executeViewString);
|
|
267
|
-
/*const executeView = `select * from ${originName} where ${originKey} = ${newEvent.dataId}`;
|
|
268
|
-
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);
|
|
269
281
|
return {
|
|
270
282
|
...resExecuteView[0]
|
|
271
283
|
};
|
|
@@ -298,6 +310,7 @@ __decorate([
|
|
|
298
310
|
(0, core_1.DbSettings)('Orm'),
|
|
299
311
|
(0, core_1.ReadOnly)(false),
|
|
300
312
|
(0, core_1.Log)(true),
|
|
313
|
+
(0, core_1.ApiDoc)(FlowNdApiDocs_1.nodeAddDoc),
|
|
301
314
|
__metadata("design:type", Function),
|
|
302
315
|
__metadata("design:paramtypes", [Object, typeorm_1.EntityManager]),
|
|
303
316
|
__metadata("design:returntype", Promise)
|
|
@@ -307,6 +320,7 @@ __decorate([
|
|
|
307
320
|
(0, core_1.DbSettings)('Orm'),
|
|
308
321
|
(0, core_1.ReadOnly)(false),
|
|
309
322
|
(0, core_1.Log)(true),
|
|
323
|
+
(0, core_1.ApiDoc)(FlowNdApiDocs_1.nodeAddActionConfigJsonDoc),
|
|
310
324
|
__metadata("design:type", Function),
|
|
311
325
|
__metadata("design:paramtypes", [Object, typeorm_1.EntityManager]),
|
|
312
326
|
__metadata("design:returntype", Promise)
|
|
@@ -316,6 +330,7 @@ __decorate([
|
|
|
316
330
|
(0, core_1.DbSettings)('Orm'),
|
|
317
331
|
(0, core_1.ReadOnly)(false),
|
|
318
332
|
(0, core_1.Log)(true),
|
|
333
|
+
(0, core_1.ApiDoc)(FlowNdApiDocs_1.nodeGetParameterizedNodeDoc),
|
|
319
334
|
__metadata("design:type", Function),
|
|
320
335
|
__metadata("design:paramtypes", [Object, typeorm_1.EntityManager]),
|
|
321
336
|
__metadata("design:returntype", Promise)
|
|
@@ -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,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pxp-nd/flow — OpenAPI (@ApiDoc) payloads for flow-nd controllers (spec tempo-04).
|
|
3
|
+
* Vendor is session-scoped; do not document vendorId / _vendorId as inputs.
|
|
4
|
+
*/
|
|
5
|
+
import { ApiDocSpec } from '@pxp-nd/core';
|
|
6
|
+
export declare const flowListTemplatesDoc: ApiDocSpec;
|
|
7
|
+
export declare const flowGetDoc: ApiDocSpec;
|
|
8
|
+
export declare const flowGetFlowRenderDoc: ApiDocSpec;
|
|
9
|
+
export declare const flowAddDoc: ApiDocSpec;
|
|
10
|
+
export declare const flowSaveFlowDoc: ApiDocSpec;
|
|
11
|
+
export declare const flowSaveFlowNameDoc: ApiDocSpec;
|
|
12
|
+
export declare const flowInsertEventFlowDoc: ApiDocSpec;
|
|
13
|
+
export declare const flowChangeStatusFlowDoc: ApiDocSpec;
|
|
14
|
+
export declare const flowDeleteFlowDoc: ApiDocSpec;
|
|
15
|
+
export declare const nodeAddDoc: ApiDocSpec;
|
|
16
|
+
export declare const nodeAddActionConfigJsonDoc: ApiDocSpec;
|
|
17
|
+
export declare const nodeGetParameterizedNodeDoc: ApiDocSpec;
|
|
18
|
+
export declare const actionGetActionsDoc: ApiDocSpec;
|
|
19
|
+
export declare const actionGetFieldMapByActionDoc: ApiDocSpec;
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @pxp-nd/flow — OpenAPI (@ApiDoc) payloads for flow-nd controllers (spec tempo-04).
|
|
4
|
+
* Vendor is session-scoped; do not document vendorId / _vendorId as inputs.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.actionGetFieldMapByActionDoc = exports.actionGetActionsDoc = exports.nodeGetParameterizedNodeDoc = exports.nodeAddActionConfigJsonDoc = exports.nodeAddDoc = exports.flowDeleteFlowDoc = exports.flowChangeStatusFlowDoc = exports.flowInsertEventFlowDoc = exports.flowSaveFlowNameDoc = exports.flowSaveFlowDoc = exports.flowAddDoc = exports.flowGetFlowRenderDoc = exports.flowGetDoc = exports.flowListTemplatesDoc = void 0;
|
|
8
|
+
exports.flowListTemplatesDoc = {
|
|
9
|
+
operationId: 'flow-nd.Flow.listTemplates',
|
|
10
|
+
summary: 'List templates',
|
|
11
|
+
description: 'Lists templates the session vendor can use: its own and the shared ones. Returns { data, count }. Defaults: _type \'template\', sort \'flowId\', dir \'ASC\', limit 50.',
|
|
12
|
+
params: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
_templateType: { type: 'string', enum: ['flow', 'funnel'] },
|
|
16
|
+
_type: { type: 'string', enum: ['template'] },
|
|
17
|
+
_publish: { type: 'string', enum: ['Y', 'N'], description: 'Y = only published templates.' },
|
|
18
|
+
_isActive: { type: 'integer', enum: [0, 1] },
|
|
19
|
+
genericFilterFields: { type: 'string', enum: ['name', 'description'] },
|
|
20
|
+
genericFilterValue: { type: 'string' },
|
|
21
|
+
filter: { type: 'string', description: 'JSON string { items, logicOperator }, as for the flow list.' },
|
|
22
|
+
start: { type: 'integer', minimum: 0 },
|
|
23
|
+
limit: { type: 'integer', minimum: 1 },
|
|
24
|
+
sort: { type: 'string', enum: ['flowId', 'name', 'createdAt', 'modifiedAt'] },
|
|
25
|
+
dir: { type: 'string', enum: ['ASC', 'DESC'] },
|
|
26
|
+
},
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
},
|
|
29
|
+
readOnly: true,
|
|
30
|
+
permission: null,
|
|
31
|
+
};
|
|
32
|
+
exports.flowGetDoc = {
|
|
33
|
+
operationId: 'flow-nd.Flow.get',
|
|
34
|
+
summary: 'Nodes of a flow',
|
|
35
|
+
description: 'Returns the nodes of one flow as an array of { node, connections }: connections are the IDs of child nodes, node.valuesForShowingInNode holds the main configured values.',
|
|
36
|
+
params: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: { flowId: { type: 'integer', minimum: 1 } },
|
|
39
|
+
required: ['flowId'],
|
|
40
|
+
additionalProperties: false,
|
|
41
|
+
},
|
|
42
|
+
readOnly: true,
|
|
43
|
+
permission: null,
|
|
44
|
+
};
|
|
45
|
+
exports.flowGetFlowRenderDoc = {
|
|
46
|
+
operationId: 'flow-nd.Flow.getFlowRender',
|
|
47
|
+
summary: 'Editor board of a flow',
|
|
48
|
+
description: 'Returns { flow, board } as used by the flow editor. board.columns[\'2-column-nodes-configured\'].taskIds is the current node order (\'node-<nodeId>\'); board.columns[\'1-column-config-nodes\'].taskIds are the steps that can be added next. Same behaviour for GET and POST.',
|
|
49
|
+
params: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: { flowId: { type: 'integer', minimum: 1 } },
|
|
52
|
+
required: ['flowId'],
|
|
53
|
+
additionalProperties: false,
|
|
54
|
+
},
|
|
55
|
+
readOnly: true,
|
|
56
|
+
permission: null,
|
|
57
|
+
};
|
|
58
|
+
exports.flowAddDoc = {
|
|
59
|
+
operationId: 'flow-nd.Flow.add',
|
|
60
|
+
summary: 'Create a flow',
|
|
61
|
+
description: 'Creates a flow for the session vendor. This is the default create route of the Flow model: it saves the fields it receives. Returns the saved flow row, including flowId.',
|
|
62
|
+
params: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
properties: {
|
|
65
|
+
name: { type: 'string', minLength: 1, maxLength: 255 },
|
|
66
|
+
description: { type: 'string' },
|
|
67
|
+
templateType: { type: 'string', enum: ['flow', 'funnel'] },
|
|
68
|
+
type: { type: 'string', enum: ['custom', 'template'] },
|
|
69
|
+
status: { type: 'string', enum: ['off', 'on'] },
|
|
70
|
+
},
|
|
71
|
+
required: ['name', 'templateType', 'type'],
|
|
72
|
+
additionalProperties: false,
|
|
73
|
+
},
|
|
74
|
+
readOnly: false,
|
|
75
|
+
permission: null,
|
|
76
|
+
};
|
|
77
|
+
exports.flowSaveFlowDoc = {
|
|
78
|
+
operationId: 'flow-nd.Flow.saveFlow',
|
|
79
|
+
summary: 'Add or remove a step',
|
|
80
|
+
description: 'Adds or removes a step in a flow, as the flow editor does. The flow must be off, otherwise 400 \'Please turn off the flow before make a change\'. To add a step (origin \'cloneTask\'): taskIds is the full new order with \'new\' at the position of the new step, and tasks.new holds the flow ID and the action code. To remove a step (origin \'deleteTask\'): taskIds is the order without that node and deleteId is its ID. Other keys of tasks are ignored. Returns { success: true, nodeId } (the new node ID, or null).',
|
|
81
|
+
params: {
|
|
82
|
+
type: 'object',
|
|
83
|
+
properties: {
|
|
84
|
+
board: {
|
|
85
|
+
type: 'object',
|
|
86
|
+
properties: {
|
|
87
|
+
origin: { type: 'string', enum: ['cloneTask', 'deleteTask'] },
|
|
88
|
+
deleteId: { type: 'string', pattern: '^node-[0-9]+$', description: 'Only for deleteTask.' },
|
|
89
|
+
columns: {
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties: {
|
|
92
|
+
'2-column-nodes-configured': {
|
|
93
|
+
type: 'object',
|
|
94
|
+
properties: {
|
|
95
|
+
taskIds: {
|
|
96
|
+
type: 'array',
|
|
97
|
+
items: { type: 'string', pattern: '^(new|node-[0-9]+)$' },
|
|
98
|
+
description: 'Node order. Existing nodes are \'node-<nodeId>\'; for cloneTask \'new\' once.',
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
required: ['taskIds'],
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
required: ['2-column-nodes-configured'],
|
|
105
|
+
},
|
|
106
|
+
tasks: {
|
|
107
|
+
type: 'object',
|
|
108
|
+
properties: {
|
|
109
|
+
new: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: {
|
|
112
|
+
flowId: { type: 'integer', minimum: 1 },
|
|
113
|
+
action: {
|
|
114
|
+
type: 'object',
|
|
115
|
+
properties: { code: { type: 'string', description: 'Action code from Action/getActions.' } },
|
|
116
|
+
required: ['code'],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
required: ['flowId', 'action'],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
required: ['origin', 'columns'],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
required: ['board'],
|
|
128
|
+
additionalProperties: false,
|
|
129
|
+
},
|
|
130
|
+
readOnly: false,
|
|
131
|
+
permission: null,
|
|
132
|
+
};
|
|
133
|
+
exports.flowSaveFlowNameDoc = {
|
|
134
|
+
operationId: 'flow-nd.Flow.saveFlowName',
|
|
135
|
+
summary: 'Rename a flow',
|
|
136
|
+
description: 'Renames a flow. The flow must be off (400 otherwise). For a template it also saves code, description, icon and flowType. Returns { success: true } or { success: false }.',
|
|
137
|
+
params: {
|
|
138
|
+
type: 'object',
|
|
139
|
+
properties: {
|
|
140
|
+
flowId: { type: 'integer', minimum: 1 },
|
|
141
|
+
name: { type: 'string', minLength: 1, maxLength: 255 },
|
|
142
|
+
code: { type: 'string', description: 'Templates only.' },
|
|
143
|
+
description: { type: 'string', description: 'Templates only.' },
|
|
144
|
+
icon: { type: 'string', description: 'Templates only.' },
|
|
145
|
+
flowType: { type: 'string', description: 'Templates only.' },
|
|
146
|
+
},
|
|
147
|
+
required: ['flowId', 'name'],
|
|
148
|
+
additionalProperties: false,
|
|
149
|
+
},
|
|
150
|
+
readOnly: false,
|
|
151
|
+
permission: null,
|
|
152
|
+
};
|
|
153
|
+
exports.flowInsertEventFlowDoc = {
|
|
154
|
+
operationId: 'flow-nd.Flow.insertEventFlow',
|
|
155
|
+
summary: 'Add the trigger of a flow',
|
|
156
|
+
description: 'Creates the first node (isInit = \'Y\') of a flow from an EVENT action. Returns { success: true, nodeId } or { success: false, msg }.',
|
|
157
|
+
params: {
|
|
158
|
+
type: 'object',
|
|
159
|
+
properties: {
|
|
160
|
+
flowId: { type: 'integer', minimum: 1 },
|
|
161
|
+
actionId: { type: 'integer', minimum: 1, description: 'An active EVENT action (Action/getActions with actionTypeName \'EVENT\').' },
|
|
162
|
+
},
|
|
163
|
+
required: ['flowId', 'actionId'],
|
|
164
|
+
additionalProperties: false,
|
|
165
|
+
},
|
|
166
|
+
readOnly: false,
|
|
167
|
+
permission: null,
|
|
168
|
+
};
|
|
169
|
+
exports.flowChangeStatusFlowDoc = {
|
|
170
|
+
operationId: 'flow-nd.Flow.changeStatusFlow',
|
|
171
|
+
summary: 'Turn a flow on or off',
|
|
172
|
+
description: 'Toggles the flow status between \'off\' and \'on\'. A flow that is on runs for real contacts (sends emails and messages). Turning on needs at least 2 active steps (400 otherwise). Returns { success: true } or { success: false }.',
|
|
173
|
+
params: {
|
|
174
|
+
type: 'object',
|
|
175
|
+
properties: { flowId: { type: 'integer', minimum: 1 } },
|
|
176
|
+
required: ['flowId'],
|
|
177
|
+
additionalProperties: false,
|
|
178
|
+
},
|
|
179
|
+
readOnly: false,
|
|
180
|
+
permission: null,
|
|
181
|
+
};
|
|
182
|
+
exports.flowDeleteFlowDoc = {
|
|
183
|
+
operationId: 'flow-nd.Flow.deleteFlow',
|
|
184
|
+
summary: 'Delete a flow',
|
|
185
|
+
description: 'Soft-deletes a flow: the flow, its instances, nodes and connections become inactive. The flow must be off (400 otherwise). Returns { success: true } or { success: false }.',
|
|
186
|
+
params: {
|
|
187
|
+
type: 'object',
|
|
188
|
+
properties: { flowId: { type: 'integer', minimum: 1 } },
|
|
189
|
+
required: ['flowId'],
|
|
190
|
+
additionalProperties: false,
|
|
191
|
+
},
|
|
192
|
+
readOnly: false,
|
|
193
|
+
permission: null,
|
|
194
|
+
};
|
|
195
|
+
exports.nodeAddDoc = {
|
|
196
|
+
operationId: 'flow-nd.Node.add',
|
|
197
|
+
summary: 'Create a node with parent connections',
|
|
198
|
+
description: 'Creates a node in a flow. For each parent, the parent\'s existing child connections are moved to the new node and a connection parent → new node is added. Every parentId must be a node of the same flow. The flow editor uses Flow/saveFlow instead. Returns the saved node row, including nodeId.',
|
|
199
|
+
params: {
|
|
200
|
+
type: 'object',
|
|
201
|
+
properties: {
|
|
202
|
+
flowId: { type: 'integer', minimum: 1 },
|
|
203
|
+
actionId: { type: 'integer', minimum: 1 },
|
|
204
|
+
isInit: { type: 'string', enum: ['Y', 'N'] },
|
|
205
|
+
isEnd: { type: 'string', enum: ['Y', 'N'] },
|
|
206
|
+
parents: {
|
|
207
|
+
type: 'array',
|
|
208
|
+
items: {
|
|
209
|
+
type: 'object',
|
|
210
|
+
properties: {
|
|
211
|
+
parentId: { type: 'integer', minimum: 1 },
|
|
212
|
+
condition: { type: 'string' },
|
|
213
|
+
},
|
|
214
|
+
required: ['parentId'],
|
|
215
|
+
additionalProperties: false,
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
required: ['flowId', 'actionId', 'isInit', 'isEnd'],
|
|
220
|
+
additionalProperties: false,
|
|
221
|
+
},
|
|
222
|
+
readOnly: false,
|
|
223
|
+
permission: null,
|
|
224
|
+
};
|
|
225
|
+
exports.nodeAddActionConfigJsonDoc = {
|
|
226
|
+
operationId: 'flow-nd.Node.AddActionConfigJson',
|
|
227
|
+
summary: 'Save the configuration of a step',
|
|
228
|
+
description: 'Saves the configuration of one node. The flow must be off (400 otherwise). The step fields are sent as top-level keys next to nodeId. __metadata maps a field name to { name } for fields that hold a merge variable; such a field is stored as \'{{ <name> }}\'. The step\'s validation controller runs before saving. Returns { actionConfigJson, success: true, nodeId, msg }.',
|
|
229
|
+
params: {
|
|
230
|
+
type: 'object',
|
|
231
|
+
properties: {
|
|
232
|
+
nodeId: { type: 'integer', minimum: 1 },
|
|
233
|
+
__metadata: {
|
|
234
|
+
type: 'object',
|
|
235
|
+
additionalProperties: {
|
|
236
|
+
type: 'object',
|
|
237
|
+
properties: { name: { type: 'string' } },
|
|
238
|
+
required: ['name'],
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
required: ['nodeId', '__metadata'],
|
|
243
|
+
additionalProperties: true,
|
|
244
|
+
},
|
|
245
|
+
readOnly: false,
|
|
246
|
+
permission: null,
|
|
247
|
+
};
|
|
248
|
+
exports.nodeGetParameterizedNodeDoc = {
|
|
249
|
+
operationId: 'flow-nd.Node.getParameterizedNode',
|
|
250
|
+
summary: 'Step with its field schema',
|
|
251
|
+
description: 'Returns a node with what is needed to configure it: the node, its merged current values, schemaJsonObject (the field schema of the step, with substitutions applied) and the merge fields available from the trigger of the flow. Does not change data.',
|
|
252
|
+
params: {
|
|
253
|
+
type: 'object',
|
|
254
|
+
properties: {
|
|
255
|
+
nodeId: { type: 'integer', minimum: 1 },
|
|
256
|
+
substitutionsSchemaJson: {
|
|
257
|
+
type: 'object',
|
|
258
|
+
description: 'Values substituted into the step\'s schema template.',
|
|
259
|
+
properties: {
|
|
260
|
+
locationId: { type: 'integer', minimum: 1 },
|
|
261
|
+
keyword: { type: 'string' },
|
|
262
|
+
},
|
|
263
|
+
additionalProperties: false,
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
required: ['nodeId'],
|
|
267
|
+
additionalProperties: false,
|
|
268
|
+
},
|
|
269
|
+
readOnly: true,
|
|
270
|
+
permission: null,
|
|
271
|
+
};
|
|
272
|
+
exports.actionGetActionsDoc = {
|
|
273
|
+
operationId: 'flow-nd.Action.getActions',
|
|
274
|
+
summary: 'Catalog of step types',
|
|
275
|
+
description: 'Returns { data: Action[] }; each action has actionId, code, name, schemaJson (field schema of the step), configJsonTemplate and actionType. Filter by actionTypeName, for example \'EVENT\' for triggers.',
|
|
276
|
+
params: {
|
|
277
|
+
type: 'object',
|
|
278
|
+
properties: {
|
|
279
|
+
actionTypeName: { type: 'string', description: 'For example EVENT, PAGE, CHAINED FLOW.' },
|
|
280
|
+
actionId: { type: 'integer', minimum: 1 },
|
|
281
|
+
},
|
|
282
|
+
additionalProperties: false,
|
|
283
|
+
},
|
|
284
|
+
readOnly: true,
|
|
285
|
+
permission: null,
|
|
286
|
+
};
|
|
287
|
+
exports.actionGetFieldMapByActionDoc = {
|
|
288
|
+
operationId: 'flow-nd.Action.getFieldMapByAction',
|
|
289
|
+
summary: 'Merge fields of an origin',
|
|
290
|
+
description: 'Returns the field-map rows (merge fields) of an origin, the data source of a trigger (Action.originName).',
|
|
291
|
+
params: {
|
|
292
|
+
type: 'object',
|
|
293
|
+
properties: { originName: { type: 'string', minLength: 1 } },
|
|
294
|
+
required: ['originName'],
|
|
295
|
+
additionalProperties: false,
|
|
296
|
+
},
|
|
297
|
+
readOnly: true,
|
|
298
|
+
permission: null,
|
|
299
|
+
};
|
|
@@ -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.
|
|
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
|
+
});
|