@pxp-nd/flow 1.1.29 → 1.1.31

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.
@@ -14,6 +14,7 @@
14
14
  * Last modified :
15
15
  * ISSUE DATE NAME DESCRIPTION
16
16
  * 9141239986 16-May-2025 Mercedes Zambrana Save flagName and FlagValue
17
+ * EF-23 08-Oct-2026 Favio Figueroa GET_LOCK instead of flag, skipped=200, axios timeout
17
18
  */
18
19
  var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
19
20
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -39,16 +40,11 @@ const Event_1 = __importDefault(require("../entity/Event"));
39
40
  const Flow_1 = __importDefault(require("../entity/Flow"));
40
41
  const Action_1 = __importDefault(require("../entity/Action"));
41
42
  const FlowLog_1 = __importDefault(require("../entity/FlowLog"));
43
+ const dbLock_1 = require("../helpers/dbLock");
42
44
  class FlowInstance extends core_1.Controller {
43
45
  async processPending(params, manager) {
44
- const flag = await common_1.GlobalData.findOne({ data: 'wf_processing_pending_flows_flag' });
45
- if (flag) {
46
- if (flag.value == 'true') {
47
- throw new core_1.PxpError(400, 'Already processing pending flows');
48
- }
49
- flag.value = 'true';
50
- let flowInstanceIdProcessing = -1;
51
- await (0, typeorm_1.getManager)().save(flag);
46
+ let flowInstanceIdProcessing = -1;
47
+ const { acquired } = await (0, dbLock_1.withDbLock)('wf_process_pending', async () => {
52
48
  try {
53
49
  await this.eventProcesss(manager);
54
50
  const maxFlows = await common_1.GlobalData.findOne({ data: 'wf_max_concurrent_flows_process' });
@@ -69,17 +65,17 @@ class FlowInstance extends core_1.Controller {
69
65
  }
70
66
  }
71
67
  catch (error) {
72
- flag.value = 'false';
73
- await (0, typeorm_1.getManager)().save(flag);
68
+ // The lock is released by withDbLock's finally block
74
69
  const log = new FlowLog_1.default();
75
70
  log.errorCustomMessage = `Error executing ${flowInstanceIdProcessing} in FlowInstance-> processPending `;
76
71
  log.errorMessage = error.message;
77
72
  log.stackTrace = error.stack || '';
78
- (0, typeorm_1.getManager)().save(log);
73
+ await (0, typeorm_1.getManager)().save(log).catch(e => console.error('[flow] FlowLog save failed', e.message));
79
74
  throw new core_1.PxpError(400, 'Error processing flow: ' + flowInstanceIdProcessing);
80
75
  }
81
- flag.value = 'false';
82
- await (0, typeorm_1.getManager)().save(flag);
76
+ });
77
+ if (!acquired) {
78
+ return { success: true, skipped: true, reason: 'already running' };
83
79
  }
84
80
  return { success: true };
85
81
  }
@@ -115,7 +111,7 @@ class FlowInstance extends core_1.Controller {
115
111
  log.errorCustomMessage = `Error processing event ${newEvent.eventId} in FlowInstance-> eventProcesss `;
116
112
  log.errorMessage = error.message;
117
113
  log.stackTrace = error.stack || '';
118
- (0, typeorm_1.getManager)().save(log);
114
+ await (0, typeorm_1.getManager)().save(log).catch(e => console.error('[flow] FlowLog save failed', e.message));
119
115
  }
120
116
  }
121
117
  }
@@ -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,6 +36,7 @@ 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");
38
40
  let Node = class Node extends core_1.Controller {
39
41
  async getNodeData(node, dataId, manager) {
40
42
  const { action: { originName, originKey } } = node;
@@ -113,6 +115,10 @@ let Node = class Node extends core_1.Controller {
113
115
  }
114
116
  async getParameterizedNode(params, manager) {
115
117
  lodash_1.default.templateSettings.interpolate = /{{([\s\S]+?)}}/g;
118
+ const nid = Number(params.nodeId);
119
+ if (!Number.isInteger(nid) || nid < 1) {
120
+ throw new core_1.PxpError(400, 'nodeId is required and must be a positive integer');
121
+ }
116
122
  const { nodeId, flowId, substitutionsSchemaJson = {} } = params;
117
123
  const tNodeData = (0, core_1.__)(Node_1.default.findOne(nodeId));
118
124
  const nodeData = await tNodeData;
@@ -298,6 +304,7 @@ __decorate([
298
304
  (0, core_1.DbSettings)('Orm'),
299
305
  (0, core_1.ReadOnly)(false),
300
306
  (0, core_1.Log)(true),
307
+ (0, core_1.ApiDoc)(FlowNdApiDocs_1.nodeAddDoc),
301
308
  __metadata("design:type", Function),
302
309
  __metadata("design:paramtypes", [Object, typeorm_1.EntityManager]),
303
310
  __metadata("design:returntype", Promise)
@@ -307,6 +314,7 @@ __decorate([
307
314
  (0, core_1.DbSettings)('Orm'),
308
315
  (0, core_1.ReadOnly)(false),
309
316
  (0, core_1.Log)(true),
317
+ (0, core_1.ApiDoc)(FlowNdApiDocs_1.nodeAddActionConfigJsonDoc),
310
318
  __metadata("design:type", Function),
311
319
  __metadata("design:paramtypes", [Object, typeorm_1.EntityManager]),
312
320
  __metadata("design:returntype", Promise)
@@ -316,6 +324,7 @@ __decorate([
316
324
  (0, core_1.DbSettings)('Orm'),
317
325
  (0, core_1.ReadOnly)(false),
318
326
  (0, core_1.Log)(true),
327
+ (0, core_1.ApiDoc)(FlowNdApiDocs_1.nodeGetParameterizedNodeDoc),
319
328
  __metadata("design:type", Function),
320
329
  __metadata("design:paramtypes", [Object, typeorm_1.EntityManager]),
321
330
  __metadata("design:returntype", Promise)
@@ -13,6 +13,7 @@
13
13
  * Last modified :
14
14
  * ISSUE DATE AUTHOR DESCRIPTION
15
15
  * 25-Apr-2025 8914754251 Mercedes Zambrana Validate and use node?.action?.controllerFunction
16
+ * 08-Oct-2026 EF-23 Favio Figueroa GET_LOCK instead of flag, skipped=200, axios timeout
16
17
  */
17
18
  import { EntityManager } from "typeorm";
18
19
  import { Controller } from "@pxp-nd/core";
@@ -14,6 +14,7 @@
14
14
  * Last modified :
15
15
  * ISSUE DATE AUTHOR DESCRIPTION
16
16
  * 25-Apr-2025 8914754251 Mercedes Zambrana Validate and use node?.action?.controllerFunction
17
+ * 08-Oct-2026 EF-23 Favio Figueroa GET_LOCK instead of flag, skipped=200, axios timeout
17
18
  */
18
19
  var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
19
20
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -40,6 +41,7 @@ const ActionType_1 = __importDefault(require("../entity/ActionType"));
40
41
  const Action_1 = __importDefault(require("../entity/Action"));
41
42
  const lodash_1 = __importDefault(require("lodash"));
42
43
  const FlowInstance_1 = __importDefault(require("../entity/FlowInstance"));
44
+ const dbLock_1 = require("../helpers/dbLock");
43
45
  //TODO replace this flow-nd need tobe independent library
44
46
  const common_1 = require("@pxp-nd/common");
45
47
  let NodeInstance = class NodeInstance extends core_1.Controller {
@@ -101,6 +103,7 @@ let NodeInstance = class NodeInstance extends core_1.Controller {
101
103
  "Content-Type": "application/json",
102
104
  },
103
105
  data: mergeJson,
106
+ timeout: Number(process.env.FLOW_ACTION_TIMEOUT_MS || 30000),
104
107
  };
105
108
  const resControllerAxios = await (0, core_1.__)((0, axios_1.default)(config));
106
109
  if ('data' in resControllerAxios && resControllerAxios.data) {
@@ -120,6 +123,7 @@ let NodeInstance = class NodeInstance extends core_1.Controller {
120
123
  "Content-Type": "application/json",
121
124
  },
122
125
  data: mergeJson,
126
+ timeout: Number(process.env.FLOW_ACTION_TIMEOUT_MS || 30000),
123
127
  };
124
128
  const resControllerAxios = await (0, core_1.__)((0, axios_1.default)(config));
125
129
  if ('data' in resControllerAxios && resControllerAxios.data) {
@@ -152,21 +156,13 @@ let NodeInstance = class NodeInstance extends core_1.Controller {
152
156
  log.errorCustomMessage = `Error executing ${flowInstance.flowInstanceId} in NodeInstance-> RecursiveInstance `;
153
157
  log.errorMessage = error.message;
154
158
  log.stackTrace = error.stack || '';
155
- (0, typeorm_1.getManager)().save(log);
159
+ await (0, typeorm_1.getManager)().save(log).catch(e => console.error('[flow] FlowLog save failed', e.message));
156
160
  }
157
161
  return {};
158
162
  }
159
163
  async ProcessDelay(params, manager) {
160
- const flag = await common_1.GlobalData.findOne({
161
- data: "wf_processing_delay_nodes_flag",
162
- });
163
- if (flag) {
164
- if (flag.value == "true") {
165
- throw new core_1.PxpError(400, "Already processing pending flows");
166
- }
167
- flag.value = "true";
168
- await (0, typeorm_1.getManager)().save(flag);
169
- let nodeInstanceIdProcessing = -1;
164
+ let nodeInstanceIdProcessing = -1;
165
+ const { acquired } = await (0, dbLock_1.withDbLock)("wf_process_delay", async () => {
170
166
  try {
171
167
  const maxNodes = await common_1.GlobalData.findOne({
172
168
  data: "wf_max_concurrent_delay_process",
@@ -216,12 +212,12 @@ let NodeInstance = class NodeInstance extends core_1.Controller {
216
212
  }
217
213
  }
218
214
  catch (error) {
219
- flag.value = "false";
220
- await (0, typeorm_1.getManager)().save(flag);
215
+ // The lock is released by withDbLock's finally block
221
216
  throw new core_1.PxpError(400, "Error processing node: " + nodeInstanceIdProcessing);
222
217
  }
223
- flag.value = "false";
224
- await (0, typeorm_1.getManager)().save(flag);
218
+ });
219
+ if (!acquired) {
220
+ return { success: true, skipped: true, reason: "already running" };
225
221
  }
226
222
  return { success: true };
227
223
  }
@@ -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,4 @@
1
+ export declare function withDbLock<T>(name: string, fn: () => Promise<T>): Promise<{
2
+ acquired: boolean;
3
+ result?: T;
4
+ }>;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withDbLock = void 0;
4
+ /**
5
+ * Named MySQL lock helper (GET_LOCK / RELEASE_LOCK).
6
+ *
7
+ * GET_LOCK is bound to the MySQL session, so the lock is taken and released on the
8
+ * same dedicated QueryRunner. Never use a transaction manager or getManager() here:
9
+ * those may run on a different pooled connection.
10
+ *
11
+ * ISSUE DATE AUTHOR DESCRIPTION
12
+ * EF-23 08-Oct-2026 Favio Figueroa GET_LOCK instead of flag, skipped=200, axios timeout
13
+ */
14
+ const typeorm_1 = require("typeorm");
15
+ async function withDbLock(name, fn) {
16
+ var _a, _b, _c, _d;
17
+ const qr = (0, typeorm_1.getConnection)(process.env.DB_WRITE_CONNECTION_NAME).createQueryRunner();
18
+ await qr.connect();
19
+ let got = 0;
20
+ try {
21
+ // GET_LOCK names are server-wide; prefix with the schema so environments sharing a MySQL instance do not block each other
22
+ const rows = await qr.query("SELECT GET_LOCK(CONCAT(DATABASE(), ':', ?), 0) AS got", [name]);
23
+ got = Number((_b = (_a = rows === null || rows === void 0 ? void 0 : rows[0]) === null || _a === void 0 ? void 0 : _a.got) !== null && _b !== void 0 ? _b : 0);
24
+ if (got !== 1)
25
+ return { acquired: false };
26
+ const result = await fn();
27
+ return { acquired: true, result };
28
+ }
29
+ finally {
30
+ if (got === 1) {
31
+ const rel = await qr
32
+ .query("SELECT RELEASE_LOCK(CONCAT(DATABASE(), ':', ?)) AS rel", [name])
33
+ .catch(() => [{ rel: 0 }]);
34
+ if (Number((_d = (_c = rel === null || rel === void 0 ? void 0 : rel[0]) === null || _c === void 0 ? void 0 : _c.rel) !== null && _d !== void 0 ? _d : 0) !== 1) {
35
+ // Lock could not be released on this session: drop the physical connection
36
+ // so MySQL frees the lock instead of returning it to the pool still held.
37
+ const c = qr.databaseConnection;
38
+ if (c === null || c === void 0 ? void 0 : c.destroy)
39
+ c.destroy();
40
+ }
41
+ }
42
+ await qr.release().catch(() => { });
43
+ }
44
+ }
45
+ exports.withDbLock = withDbLock;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pxp-nd/flow",
3
- "version": "1.1.29",
3
+ "version": "1.1.31",
4
4
  "description": "Workflow module for pxp-nd framework",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@pxp-nd/common": "^1.1.4",
40
- "@pxp-nd/core": "^1.1.13",
40
+ "@pxp-nd/core": "^1.2.94",
41
41
  "axios": "^0.21.1",
42
42
  "lodash": "^4.17.21",
43
43
  "typeorm": "^0.2.36"