@pxp-nd/flow 1.1.31 → 1.1.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -68,6 +68,7 @@ const NodeConnection_1 = __importDefault(require("../entity/NodeConnection"));
68
68
  const Flow_1 = __importDefault(require("../entity/Flow"));
69
69
  const FlowInstance_1 = __importDefault(require("../entity/FlowInstance"));
70
70
  const Node_2 = __importDefault(require("./Node"));
71
+ const flowFilter_1 = require("../helpers/flowFilter");
71
72
  const axios_1 = __importDefault(require("axios"));
72
73
  //import {configFacebookStrategy} from "./passport-facebook";
73
74
  const FlowNdApiDocs_1 = require("../helpers/FlowNdApiDocs");
@@ -192,14 +193,10 @@ let Flow = class Flow extends core_1.Controller {
192
193
  dataFlow.modifiedBy = this.user.username;
193
194
  const updFlow = await (0, core_1.__)(manager.save(dataFlow));
194
195
  if (updFlow) {
195
- let flowInstance = `UPDATE twf_flow_instance SET
196
- is_active = 'N' WHERE flow_id = ${updFlow.flowId}`;
197
- await manager.query(flowInstance);
196
+ await manager.query(`UPDATE twf_flow_instance SET is_active = 'N' WHERE flow_id = ?`, [updFlow.flowId]);
198
197
  const nodesToInact = await manager.find(Node_1.default, { flowId: updFlow.flowId, isActive: true });
199
198
  if (nodesToInact.length > 0) {
200
- let flowNode = `UPDATE twf_node SET
201
- is_active = 'N' WHERE flow_id = ${updFlow.flowId}`;
202
- await manager.query(flowNode);
199
+ await manager.query(`UPDATE twf_node SET is_active = 'N' WHERE flow_id = ?`, [updFlow.flowId]);
203
200
  const nodesToInact = await manager.find(Node_1.default, { flowId: updFlow.flowId });
204
201
  const originalNodeIds = nodesToInact.map((node) => node.nodeId);
205
202
  const connectionsToInact = await manager.find(NodeConnection_1.default, {
@@ -207,9 +204,7 @@ let Flow = class Flow extends core_1.Controller {
207
204
  });
208
205
  const originalNodeCIds = connectionsToInact.map((node) => node.nodeConnectionId);
209
206
  if (originalNodeCIds.length > 0) {
210
- let flowNodeC = `UPDATE twf_node_connection SET
211
- is_active = 'N' WHERE node_connection_id in ( ${originalNodeCIds})`;
212
- await manager.query(flowNodeC);
207
+ await manager.query(`UPDATE twf_node_connection SET is_active = 'N' WHERE node_connection_id IN (?)`, [originalNodeCIds]);
213
208
  }
214
209
  }
215
210
  return { success: true };
@@ -328,9 +323,9 @@ let Flow = class Flow extends core_1.Controller {
328
323
  });
329
324
  if (connectionsToDel) {
330
325
  const originalNodeCIds = connectionsToDel.map((node) => node.nodeConnectionId);
331
- let flowNodeC = await manager.query(`DELETE
332
- from twf_node_connection
333
- 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
+ }
334
329
  }
335
330
  }
336
331
  }
@@ -349,9 +344,9 @@ let Flow = class Flow extends core_1.Controller {
349
344
  allowModify = true;
350
345
  //If there are some nodes in node_connections after delete all records sent, to inactive node
351
346
  let connectNoDel = await (0, typeorm_1.getManager)().query(`SELECT n.node_id
352
- from twf_node_connection nd
353
- inner join twf_node n on n.node_id = nd.node_id_child
354
- 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]);
355
350
  const currentNodesIds = connectNoDel.map((record) => record.node_id);
356
351
  // Get nodes to remove
357
352
  const nodesToRemove = currentNodesIds.filter((nodeId) => !taskIds.includes(nodeId.toString()));
@@ -359,10 +354,7 @@ let Flow = class Flow extends core_1.Controller {
359
354
  if (nodesToRemove.length > 0) {
360
355
  const delNodeConnect = await manager.delete(NodeConnection_1.default, { nodeIdChild: nodesToRemove[0] });
361
356
  if (delNodeConnect) {
362
- let dNode = `UPDATE twf_node
363
- SET is_active = 'N'
364
- WHERE node_id = ${nodesToRemove[0]}`;
365
- await manager.query(dNode);
357
+ await manager.query(`UPDATE twf_node SET is_active = 'N' WHERE node_id = ?`, [nodesToRemove[0]]);
366
358
  }
367
359
  }
368
360
  if (flowIdDel.action.actionType.name === 'CHAINED FLOW') {
@@ -842,7 +834,7 @@ let Flow = class Flow extends core_1.Controller {
842
834
  const applyDynamicFilters = (queryBuilder, filters, logicOperator) => {
843
835
  filters.forEach((filter, index) => {
844
836
  const { field, operator, value } = filter;
845
- const queryField = `f.${field}`;
837
+ const queryField = `f.${(0, flowFilter_1.flowFilterColumn)(field)}`;
846
838
  const parameterName = `filterValue${index}`;
847
839
  let condition;
848
840
  switch (operator) {
@@ -1006,7 +998,7 @@ let Flow = class Flow extends core_1.Controller {
1006
998
  const applyDynamicFilters = (queryBuilder, filters, logicOperator) => {
1007
999
  filters.forEach((filter, index) => {
1008
1000
  const { field, operator, value } = filter;
1009
- const queryField = `f.${field}`;
1001
+ const queryField = `f.${(0, flowFilter_1.flowFilterColumn)(field)}`;
1010
1002
  const parameterName = `filterValue${index}`;
1011
1003
  let condition;
1012
1004
  switch (operator) {
@@ -41,6 +41,7 @@ const Flow_1 = __importDefault(require("../entity/Flow"));
41
41
  const Action_1 = __importDefault(require("../entity/Action"));
42
42
  const FlowLog_1 = __importDefault(require("../entity/FlowLog"));
43
43
  const dbLock_1 = require("../helpers/dbLock");
44
+ const sqlIdent_1 = require("../helpers/sqlIdent");
44
45
  class FlowInstance extends core_1.Controller {
45
46
  async processPending(params, manager) {
46
47
  let flowInstanceIdProcessing = -1;
@@ -89,8 +90,7 @@ class FlowInstance extends core_1.Controller {
89
90
  for (const n of node) {
90
91
  //get data of view
91
92
  const { action: { originName, originKey }, } = n;
92
- const 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))) {
@@ -37,11 +37,12 @@ const OriginName_1 = __importDefault(require("../entity/OriginName"));
37
37
  const axios_1 = __importDefault(require("axios"));
38
38
  const Flow_1 = __importDefault(require("../entity/Flow"));
39
39
  const FlowNdApiDocs_1 = require("../helpers/FlowNdApiDocs");
40
+ const sqlIdent_1 = require("../helpers/sqlIdent");
41
+ const sessionVendor_1 = require("../helpers/sessionVendor");
40
42
  let Node = class Node extends core_1.Controller {
41
43
  async getNodeData(node, dataId, manager) {
42
44
  const { action: { originName, originKey } } = node;
43
- const executeView = `select * from ${originName} where ${originKey} = ${dataId}`;
44
- const resExecuteView = await (0, core_1.__)(manager.query(executeView));
45
+ const resExecuteView = await (0, core_1.__)((0, sqlIdent_1.selectByIdent)(manager, originName, originKey, dataId));
45
46
  return JSON.stringify(resExecuteView[0]);
46
47
  }
47
48
  async add(params, manager) {
@@ -119,7 +120,14 @@ let Node = class Node extends core_1.Controller {
119
120
  if (!Number.isInteger(nid) || nid < 1) {
120
121
  throw new core_1.PxpError(400, 'nodeId is required and must be a positive integer');
121
122
  }
122
- const { nodeId, flowId, substitutionsSchemaJson = {} } = params;
123
+ const { nodeId, flowId } = params;
124
+ const requestedSubstitutions = params.substitutionsSchemaJson && typeof params.substitutionsSchemaJson === 'object'
125
+ ? { ...params.substitutionsSchemaJson }
126
+ : {};
127
+ const sessionVendor = (0, sessionVendor_1.harmonyVendorId)(this.user);
128
+ const substitutionsSchemaJson = sessionVendor != null
129
+ ? { ...requestedSubstitutions, vendorId: sessionVendor }
130
+ : requestedSubstitutions;
123
131
  const tNodeData = (0, core_1.__)(Node_1.default.findOne(nodeId));
124
132
  const nodeData = await tNodeData;
125
133
  const tNodeOrigin = await (0, core_1.__)(Node_1.default.findOne({
@@ -261,17 +269,15 @@ let Node = class Node extends core_1.Controller {
261
269
  };
262
270
  }
263
271
  async executeActionJson(params, manager) {
264
- const { nodeId, originName, originKey, fromValues } = params;
265
- // 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
+ }
266
277
  const getParameterizedNodeData = await (0, core_1.__)(this.getParameterizedNode({ nodeId: nodeId }, manager));
267
278
  const { mergeValues } = getParameterizedNodeData;
268
- const findOriginValue = Object.entries(mergeValues).find(([nameValue]) => fromValues === nameValue);
269
- const resValueObject = Object.fromEntries([findOriginValue]);
270
- const originValue = resValueObject[fromValues];
271
- const executeViewString = `select * from ${originName} where ${originKey} = ${originValue}`;
272
- const resExecuteView = await (0, typeorm_1.getManager)().query(executeViewString);
273
- /*const executeView = `select * from ${originName} where ${originKey} = ${newEvent.dataId}`;
274
- const resExecuteView = await getManager().query(executeView);*/
279
+ const originValue = mergeValues ? mergeValues[fromValues] : undefined;
280
+ const resExecuteView = await (0, sqlIdent_1.selectByIdent)((0, typeorm_1.getManager)(), nodeRow.action.originName, nodeRow.action.originKey, originValue);
275
281
  return {
276
282
  ...resExecuteView[0]
277
283
  };
@@ -42,18 +42,20 @@ const Action_1 = __importDefault(require("../entity/Action"));
42
42
  const lodash_1 = __importDefault(require("lodash"));
43
43
  const FlowInstance_1 = __importDefault(require("../entity/FlowInstance"));
44
44
  const dbLock_1 = require("../helpers/dbLock");
45
+ const sqlIdent_1 = require("../helpers/sqlIdent");
45
46
  //TODO replace this flow-nd need tobe independent library
46
47
  const common_1 = require("@pxp-nd/common");
47
48
  let NodeInstance = class NodeInstance extends core_1.Controller {
48
49
  async RecursiveInstance(params, manager) {
49
- var _a;
50
+ var _a, _b, _c, _d, _e;
50
51
  //https://lodash.com/docs/4.17.15#template
51
52
  lodash_1.default.templateSettings.interpolate = /{{([\s\S]+?)}}/g;
52
53
  //todo after this we need to get the json variables and execute the view and conditions configured in the database
53
54
  const { node, flowInstance, eventNode } = params;
54
55
  try {
55
- const 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,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.31",
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.2.94",
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
+ }