@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.
@@ -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)(false),
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';
@@ -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
- let flowInstance = `UPDATE twf_flow_instance SET
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
- let flowNode = `UPDATE twf_node SET
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
- let flowNodeC = `UPDATE twf_node_connection SET
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
- let flowNodeC = await manager.query(`DELETE
326
- from twf_node_connection
327
- WHERE node_connection_id = ${originalNodeCIds}`);
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
- from twf_node_connection nd
347
- inner join twf_node n on n.node_id = nd.node_id_child
348
- WHERE n.node_id = ${nodeDelId}`);
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
- let dNode = `UPDATE twf_node
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.${params.sort}`, params.dir);
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.${params.sort || "flowId"}`, params.dir || "ASC");
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 executeView = `select * from ${originName} where ${originKey} = ${newEvent.dataId}`;
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';
@@ -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 executeView = `select * from ${originName} where ${originKey} = ${dataId}`;
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 { nodeId, flowId, substitutionsSchemaJson = {} } = params;
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, originName, originKey, fromValues } = params;
259
- // we need to get the value from merge values with originKey
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 findOriginValue = Object.entries(mergeValues).find(([nameValue]) => fromValues === nameValue);
263
- const resValueObject = Object.fromEntries([findOriginValue]);
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 executeView = `select * from ${flowInstance.originName} where ${flowInstance.originKey} = ${flowInstance.dataId}`;
56
- const resExecuteView = await (0, core_1.__)((0, typeorm_1.getManager)().query(executeView));
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 (!!((_a = node === null || node === void 0 ? void 0 : node.action) === null || _a === void 0 ? void 0 : _a.controllerFunction)) {
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.30",
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": "echo \"Error: no test specified\" && exit 1",
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.1.13",
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
+ });
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "..",
5
+ "noEmit": true,
6
+ "types": ["jest", "node"]
7
+ },
8
+ "include": ["../src/**/*.ts", "./**/*.ts"]
9
+ }