@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.
- package/BPM_Propossal.md +566 -0
- package/README.md +437 -1
- package/dist/controllers/Action.d.ts +1 -1
- package/dist/controllers/Action.js +8 -2
- package/dist/controllers/Flow.d.ts +1 -0
- package/dist/controllers/Flow.js +39 -2
- package/dist/controllers/FlowInstance.d.ts +1 -0
- package/dist/controllers/FlowInstance.js +10 -14
- package/dist/controllers/Node.d.ts +1 -0
- package/dist/controllers/Node.js +9 -0
- package/dist/controllers/NodeInstance.d.ts +1 -0
- package/dist/controllers/NodeInstance.js +11 -15
- package/dist/helpers/FlowNdApiDocs.d.ts +19 -0
- package/dist/helpers/FlowNdApiDocs.js +299 -0
- package/dist/helpers/dbLock.d.ts +4 -0
- package/dist/helpers/dbLock.js +45 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1 +1,437 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Tempo Flows (Workflow Engine)
|
|
2
|
+
|
|
3
|
+
This document describes how automation **flows** work in Tempo: database structure, runtime pipeline, and how to trigger or assemble them.
|
|
4
|
+
|
|
5
|
+
Flows are a **node-graph automation engine**. Definitions and runtime state live in MySQL tables prefixed `twf_*`. Core entities and graph execution come from **`@pxp-nd/flow`**. Tempo adds business orchestration via `EventHq`, `FlowHQ`, and an SQS worker.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## High-level architecture
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
Business code (controllers / PHP jobs)
|
|
13
|
+
│
|
|
14
|
+
▼
|
|
15
|
+
EventHq.add({ actionId, descJobName, dataId })
|
|
16
|
+
│
|
|
17
|
+
├─► twf_event (status: pending)
|
|
18
|
+
└─► SQS FIFO (FLOW_QUEUE_URL)
|
|
19
|
+
│
|
|
20
|
+
▼
|
|
21
|
+
worker.ts → EventHq.processSingleEvent({ eventId })
|
|
22
|
+
│
|
|
23
|
+
├─► Find active twf_node rows for actionId
|
|
24
|
+
├─► Query origin view (origin_name + origin_key = dataId)
|
|
25
|
+
├─► Vendor match + event_config conditions
|
|
26
|
+
├─► Create twf_flow_instance
|
|
27
|
+
└─► NodeInstanceController.RecursiveInstance() walks the DAG
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Key Tempo files
|
|
31
|
+
|
|
32
|
+
| File | Role |
|
|
33
|
+
|------|------|
|
|
34
|
+
| `src/modules/hq-nd/controllers/EventHq.ts` | Enqueue events to SQS; process a single event into flow instances |
|
|
35
|
+
| `src/modules/hq-nd/controllers/Flow.ts` (`FlowHQ`) | Duplicate templates, chained flows, funnel lists/statistics |
|
|
36
|
+
| `src/worker.ts` | SQS consumer (started when `INIT_FLOW_WORKER=Y`) |
|
|
37
|
+
| `src/config.ts` / `ormconfig.js` | Registers `flow-nd` module and merges flow entities |
|
|
38
|
+
|
|
39
|
+
Entities such as `Flow`, `Node`, `Action`, `Event`, `FlowInstance`, `NodeInstance` are **not** redefined in Tempo; they come from `@pxp-nd/flow`.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## How execution works (summary)
|
|
44
|
+
|
|
45
|
+
1. **Fire an event**
|
|
46
|
+
Application code looks up `twf_action` by stable `code`, then calls `EventHq.add` with:
|
|
47
|
+
- `actionId` — PK of the action
|
|
48
|
+
- `dataId` — PK value in the action’s origin view (`origin_key`)
|
|
49
|
+
- `descJobName` — caller context for debugging
|
|
50
|
+
|
|
51
|
+
2. **Enqueue**
|
|
52
|
+
`EventHq.add` inserts a `twf_event` (`status = pending`) and sends a message to SQS containing `eventId`.
|
|
53
|
+
|
|
54
|
+
3. **Worker**
|
|
55
|
+
`pollQueue()` in `src/worker.ts` receives the message, deletes it from the queue, then calls `processSingleEvent` with retries (1s / 5s / 10s / 20s). Failures after retries go to Slack (`SLACK_TEMPO_ERROR`).
|
|
56
|
+
|
|
57
|
+
4. **Match flows**
|
|
58
|
+
For every active `twf_node` with that `action_id`:
|
|
59
|
+
- Load one row: `SELECT * FROM {origin_name} WHERE {origin_key} = {dataId}`
|
|
60
|
+
- Load parent `twf_flow` — must be `is_active = 1` and **`status = 'on'`**
|
|
61
|
+
- Vendor filter: `flow.vendor_id === view.vendor_id` **OR** `flow.vendor_id === 55` (global Qorus flows)
|
|
62
|
+
- Optional `event_config` filters on the action/node
|
|
63
|
+
|
|
64
|
+
5. **Run the graph**
|
|
65
|
+
Create `twf_flow_instance`, then walk `twf_node` / `twf_node_connection` via `RecursiveInstance()`. Each step creates `twf_node_instance` rows. Errors are stored in `twf_flow_log`.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Database ER diagram
|
|
70
|
+
|
|
71
|
+
```mermaid
|
|
72
|
+
erDiagram
|
|
73
|
+
twf_action_type ||--o{ twf_action : "categorizes"
|
|
74
|
+
twf_action ||--o{ twf_node : "used by"
|
|
75
|
+
twf_action ||--o{ twf_event : "triggered as"
|
|
76
|
+
twf_action ||--o{ twf_flow_instance : "started by"
|
|
77
|
+
twf_flow ||--o{ twf_node : "contains"
|
|
78
|
+
twf_flow ||--o{ twf_flow_instance : "instantiates"
|
|
79
|
+
twf_node ||--o{ twf_node_connection : "master"
|
|
80
|
+
twf_node ||--o{ twf_node_connection : "child"
|
|
81
|
+
twf_node ||--o{ twf_node_instance : "executes as"
|
|
82
|
+
twf_flow_instance ||--o{ twf_node_instance : "has steps"
|
|
83
|
+
twf_origin_name ||--o{ twf_field_map : "maps fields"
|
|
84
|
+
|
|
85
|
+
twf_action_type {
|
|
86
|
+
int action_type_id PK
|
|
87
|
+
varchar name
|
|
88
|
+
varchar schema_json
|
|
89
|
+
varchar is_start
|
|
90
|
+
varchar is_delay
|
|
91
|
+
varchar controller
|
|
92
|
+
varchar on_duplicate
|
|
93
|
+
tinyint is_active
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
twf_action {
|
|
97
|
+
int action_id PK
|
|
98
|
+
int action_type_id FK
|
|
99
|
+
varchar code "stable contract"
|
|
100
|
+
varchar name
|
|
101
|
+
varchar origin_name "DB view"
|
|
102
|
+
varchar origin_key "PK column in view"
|
|
103
|
+
varchar controller_function
|
|
104
|
+
varchar schema_json
|
|
105
|
+
varchar event_config
|
|
106
|
+
varchar config_json_template
|
|
107
|
+
tinyint is_active
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
twf_flow {
|
|
111
|
+
int flow_id PK
|
|
112
|
+
int vendor_id "0/55 template or vendor"
|
|
113
|
+
varchar code
|
|
114
|
+
varchar name
|
|
115
|
+
varchar type "template|custom"
|
|
116
|
+
varchar template_type "funnel|flow|null"
|
|
117
|
+
varchar status "must be on to run"
|
|
118
|
+
varchar enabled
|
|
119
|
+
tinyint is_active
|
|
120
|
+
varchar publish
|
|
121
|
+
varchar flow_type
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
twf_node {
|
|
125
|
+
int node_id PK
|
|
126
|
+
int flow_id FK
|
|
127
|
+
int action_id FK
|
|
128
|
+
varchar is_init "Y = entry/trigger"
|
|
129
|
+
varchar is_end
|
|
130
|
+
varchar action_config_json "runtime values"
|
|
131
|
+
varchar approval_config
|
|
132
|
+
tinyint is_active
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
twf_node_connection {
|
|
136
|
+
int node_connection_id PK
|
|
137
|
+
int node_id_master "NULL = init edge"
|
|
138
|
+
int node_id_child FK
|
|
139
|
+
varchar condition
|
|
140
|
+
tinyint is_active
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
twf_event {
|
|
144
|
+
int event_id PK
|
|
145
|
+
int action_id FK
|
|
146
|
+
int data_id "origin PK value"
|
|
147
|
+
varchar desc_job_name
|
|
148
|
+
varchar status "pending|processed"
|
|
149
|
+
tinyint is_active
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
twf_flow_instance {
|
|
153
|
+
int flow_instance_id PK
|
|
154
|
+
int flow_id FK
|
|
155
|
+
int data_id
|
|
156
|
+
int action_id
|
|
157
|
+
varchar origin_name
|
|
158
|
+
varchar origin_key
|
|
159
|
+
varchar status
|
|
160
|
+
varchar flag_name
|
|
161
|
+
int flag_value
|
|
162
|
+
tinyint is_active
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
twf_node_instance {
|
|
166
|
+
int node_instance_id PK
|
|
167
|
+
int node_id FK
|
|
168
|
+
int flow_instance_id FK
|
|
169
|
+
timestamp run_time
|
|
170
|
+
timestamp schedule
|
|
171
|
+
varchar status
|
|
172
|
+
json extra_args
|
|
173
|
+
tinyint is_active
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
twf_origin_name {
|
|
177
|
+
int origin_name_id PK
|
|
178
|
+
varchar name "view name"
|
|
179
|
+
varchar originKey
|
|
180
|
+
tinyint is_active
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
twf_field_map {
|
|
184
|
+
int field_map_id PK
|
|
185
|
+
int origin_name_id FK
|
|
186
|
+
varchar name
|
|
187
|
+
varchar alias
|
|
188
|
+
varchar type
|
|
189
|
+
tinyint is_active
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
twf_flow_log {
|
|
193
|
+
int flow_log_id PK
|
|
194
|
+
varchar error_custom_message
|
|
195
|
+
varchar error_message
|
|
196
|
+
text stack_trace
|
|
197
|
+
tinyint is_active
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
> Note: Some FK relationships are logical (enforced in application / TypeORM) rather than hard MySQL foreign keys on every column.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Runtime pipeline diagram
|
|
206
|
+
|
|
207
|
+
```mermaid
|
|
208
|
+
flowchart LR
|
|
209
|
+
A[Business code] -->|Action.findOne by code| B[EventHq.add]
|
|
210
|
+
B --> C[(twf_event pending)]
|
|
211
|
+
B --> D[SQS FLOW_QUEUE_URL]
|
|
212
|
+
D --> E[worker.ts pollQueue]
|
|
213
|
+
E --> F[EventHq.processSingleEvent]
|
|
214
|
+
F --> G{Active nodes for actionId}
|
|
215
|
+
G --> H[Query origin view by dataId]
|
|
216
|
+
H --> I{flow status=on<br/>vendor match or vendor 55}
|
|
217
|
+
I -->|yes| J[(twf_flow_instance)]
|
|
218
|
+
J --> K[RecursiveInstance DAG walk]
|
|
219
|
+
K --> L[(twf_node_instance)]
|
|
220
|
+
I -->|no| M[Skip node]
|
|
221
|
+
F -.->|errors| N[(twf_flow_log)]
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Table reference
|
|
227
|
+
|
|
228
|
+
| Table | Role |
|
|
229
|
+
|-------|------|
|
|
230
|
+
| `twf_action_type` | Category of step (EVENT, EMAIL, DELAY, PAGE, CHAINED FLOW, CONTROLLER EXECUTION, …) |
|
|
231
|
+
| `twf_action` | Reusable action / event definition; stable API via `code` |
|
|
232
|
+
| `twf_flow` | Flow definition (template or vendor custom instance) |
|
|
233
|
+
| `twf_node` | One step in a flow; points at one action; holds `action_config_json` |
|
|
234
|
+
| `twf_node_connection` | Directed edges (DAG). Init connection uses `node_id_master = NULL` |
|
|
235
|
+
| `twf_event` | Async event queue row |
|
|
236
|
+
| `twf_flow_instance` | One execution of a flow for a given `dataId` |
|
|
237
|
+
| `twf_node_instance` | Per-step execution record |
|
|
238
|
+
| `twf_origin_name` | Registry of DB views used as event data sources |
|
|
239
|
+
| `twf_field_map` | Column aliases for Harmony builder / templating |
|
|
240
|
+
| `twf_flow_log` | Execution / processing errors |
|
|
241
|
+
|
|
242
|
+
### Important `twf_flow` fields
|
|
243
|
+
|
|
244
|
+
| Field | Meaning |
|
|
245
|
+
|-------|---------|
|
|
246
|
+
| `type` | `template` (blueprint) or `custom` (vendor instance) |
|
|
247
|
+
| `template_type` | `funnel`, `flow`, or null |
|
|
248
|
+
| `status` | Must be **`on`** for `EventHq` to execute the flow |
|
|
249
|
+
| `vendor_id` | `0` / `55` for global templates; vendor id for instances. **Vendor 55** matches any vendor’s events |
|
|
250
|
+
| `code` | Optional stable identifier (e.g. `NEW_EMAIL_LIST`) |
|
|
251
|
+
|
|
252
|
+
### Important `twf_action` fields
|
|
253
|
+
|
|
254
|
+
| Field | Meaning |
|
|
255
|
+
|-------|---------|
|
|
256
|
+
| `code` | Unique string — always resolve with `Action.findOne({ code })` |
|
|
257
|
+
| `origin_name` / `origin_key` | View + PK used when resolving `dataId` |
|
|
258
|
+
| `schema_json` | UI form for Harmony flow builder |
|
|
259
|
+
| `event_config` | Optional filters: `{ "filters": [{ "originField", "nodeField" }] }` |
|
|
260
|
+
| `controller_function` | Tempo controller path for CONTROLLER EXECUTION (e.g. `hq-nd/BusinessTypeSF/sendApprovalNotification`) |
|
|
261
|
+
|
|
262
|
+
### Common `action_type_id` values (active)
|
|
263
|
+
|
|
264
|
+
| ID | Name | Notes |
|
|
265
|
+
|----|------|--------|
|
|
266
|
+
| 9 | EVENT | Trigger / listener node |
|
|
267
|
+
| 10 | EMAIL NOTIFICATION | Send email |
|
|
268
|
+
| 11 | DELAY | Wait step |
|
|
269
|
+
| 16 | PAGE | Opt-in / bridge pages |
|
|
270
|
+
| 24 | CHAINED FLOW | Invoke another flow |
|
|
271
|
+
| 26 | PUSH NOTIFICATION | Mobile push |
|
|
272
|
+
| 28 | CONTROLLER EXECUTION | Call a Tempo controller method |
|
|
273
|
+
|
|
274
|
+
`on_duplicate` on the action type drives post-processing when duplicating a flow (`duplicateEmailTemplate`, `duplicatePageTemplate`, `duplicateChainedFlow`).
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## JSON configuration layers
|
|
279
|
+
|
|
280
|
+
1. **`twf_action_type.schema_json`** — shared UI schema for the type
|
|
281
|
+
2. **`twf_action.schema_json`** — per-action UI overrides
|
|
282
|
+
3. **`twf_node.action_config_json`** — **actual values at runtime** for that node
|
|
283
|
+
|
|
284
|
+
Examples of `action_config_json`:
|
|
285
|
+
|
|
286
|
+
| Type | Typical keys |
|
|
287
|
+
|------|----------------|
|
|
288
|
+
| PAGE | `templateBuilderId`, `emailListId`, `smsListId`, `title` |
|
|
289
|
+
| EMAIL | `to`, `subject`, `templateId` |
|
|
290
|
+
| DELAY | `quantity`, `type` (`hours` / `minutes` / `days`) |
|
|
291
|
+
| CHAINED / EMAIL_JOURNEY | `flowId` |
|
|
292
|
+
| CONTROLLER / push (SF) | `subject`, `message`, `mobileActionId`, `notificationType` |
|
|
293
|
+
|
|
294
|
+
Templates may use `{{ FIELD }}` placeholders resolved from the origin view row (`config_json_template` / node config).
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Triggering a flow from TypeScript
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
import { Action, Event } from '@pxp-nd/flow';
|
|
302
|
+
import EventHq from './EventHq';
|
|
303
|
+
|
|
304
|
+
const action = await Action.findOne({ code: 'ADD EMAIL LIST ADDRESS' });
|
|
305
|
+
if (action) {
|
|
306
|
+
const eventInstance = new EventHq('hq-nd/EventHq', Event);
|
|
307
|
+
await eventInstance.add({
|
|
308
|
+
actionId: action.actionId,
|
|
309
|
+
descJobName: 'formLegato', // caller context
|
|
310
|
+
dataId: resInsert.emailListAddrId, // PK in action.origin_name
|
|
311
|
+
}, manager);
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Rules:
|
|
316
|
+
|
|
317
|
+
- Prefer **`EventHq.add()`** in Tempo (enqueues SQS). Do not bypass the queue for production event firing.
|
|
318
|
+
- Never hardcode `action_id`; always look up by `code`.
|
|
319
|
+
- `dataId` must exist in the action’s origin view.
|
|
320
|
+
- Flow must be `status = 'on'` and `is_active = 1`.
|
|
321
|
+
|
|
322
|
+
Events may also be fired from legacy PHP (e.g. billing jobs calling `hq-nd/EventHq/add`).
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Assembling / creating flows
|
|
327
|
+
|
|
328
|
+
### A. Duplicate a template (preferred for vendor instances)
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
const flowHq = new FlowHQ('hq-nd/FlowHQ');
|
|
332
|
+
const result = await flowHq.duplicateFlow(
|
|
333
|
+
{ flowId: templateFlowId, vendorId, name: 'Bridge Funnel' },
|
|
334
|
+
manager
|
|
335
|
+
);
|
|
336
|
+
// Activate if needed:
|
|
337
|
+
await manager.createQueryBuilder().update('twf_flow')
|
|
338
|
+
.set({ status: 'on' })
|
|
339
|
+
.where('flowId = :flowId', { flowId })
|
|
340
|
+
.execute();
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Template IDs often come from env:
|
|
344
|
+
|
|
345
|
+
- `TEMPLATE_BRIDGE_FUNNEL_FLOW_ID` (default `149`)
|
|
346
|
+
- `TEMPLATE_QORD_BRIDGE_FUNNEL_FLOW_ID` (default `3419`)
|
|
347
|
+
|
|
348
|
+
### B. Patch init node after duplicate
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
await manager.createQueryBuilder().update('twf_node')
|
|
352
|
+
.set({ actionConfigJson: JSON.stringify({ emailListId }) })
|
|
353
|
+
.where('flowId = :flowId AND isInit = :isInit', { flowId, isInit: 'Y' })
|
|
354
|
+
.execute();
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### C. Chained funnel → email journey
|
|
358
|
+
|
|
359
|
+
Use `FlowHQ.addChainedFlow()` (duplicates child flow, copies `emailListId`, activates).
|
|
360
|
+
|
|
361
|
+
### D. SQL registration
|
|
362
|
+
|
|
363
|
+
Typical order:
|
|
364
|
+
|
|
365
|
+
1. Optional `twf_action_type`
|
|
366
|
+
2. `twf_action` (EVENT + step actions) with `WHERE NOT EXISTS (... code ...)`
|
|
367
|
+
3. If new origin: `twf_origin_name` + `twf_field_map`
|
|
368
|
+
4. Optional full graph: `twf_flow` → init `twf_node` → connections → step nodes
|
|
369
|
+
5. Register in `src/modules/hq-nd/database/jaime-scripts.ts` as `JRR-HQ-[STORY]-[SEQ]`
|
|
370
|
+
|
|
371
|
+
Modern SF-style DML often registers **actions only** and lets the Harmony UI assemble the graph.
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## Vendor matching (important)
|
|
376
|
+
|
|
377
|
+
When processing an event, Tempo runs a flow if:
|
|
378
|
+
|
|
379
|
+
```text
|
|
380
|
+
flow.vendorId === viewVendorId OR flow.vendorId === 55
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
So:
|
|
384
|
+
|
|
385
|
+
- A **vendor-specific** flow only runs for that vendor’s data.
|
|
386
|
+
- A **vendor 55 (Qorus)** flow runs for **any** vendor that fires the matching event (global notifications).
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
## Environment variables
|
|
391
|
+
|
|
392
|
+
| Variable | Purpose |
|
|
393
|
+
|----------|---------|
|
|
394
|
+
| `FLOW_QUEUE_URL` | SQS FIFO queue for async events |
|
|
395
|
+
| `INIT_FLOW_WORKER` | `Y` starts `pollQueue()` from `server.ts` |
|
|
396
|
+
| `TEMPLATE_BRIDGE_FUNNEL_FLOW_ID` | Default bridge funnel template |
|
|
397
|
+
| `TEMPLATE_QORD_BRIDGE_FUNNEL_FLOW_ID` | Qord bridge funnel template |
|
|
398
|
+
| `SLACK_TEMPO_ERROR` | Channel for worker failures after retries |
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
## Naming conventions
|
|
403
|
+
|
|
404
|
+
- Action `code`: UPPER CASE with spaces — e.g. `NEW POSTED NEED`, `BILLING NOT PROCESSED`, `SF SEND COS PUSH`
|
|
405
|
+
- Funnel templates: `type = 'template'`, `template_type = 'funnel'`
|
|
406
|
+
- Email / automation templates: `type = 'template'`, `template_type = 'flow'` (or null)
|
|
407
|
+
- Vendor copies after `duplicateFlow`: `type = 'custom'`
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## Do / Don’t
|
|
412
|
+
|
|
413
|
+
**Do**
|
|
414
|
+
|
|
415
|
+
- Find actions by `code`
|
|
416
|
+
- Duplicate templates for vendor flows; keep them `status = 'on'` when they should run
|
|
417
|
+
- Register new origins in `twf_origin_name` (+ field maps) before using them in actions
|
|
418
|
+
- Use `EventHq.add` so processing goes through SQS
|
|
419
|
+
|
|
420
|
+
**Don’t**
|
|
421
|
+
|
|
422
|
+
- Hardcode `action_id`
|
|
423
|
+
- Auto-sync `twf_*` schema (`synchronize` stays false)
|
|
424
|
+
- Skip SQS for production event firing
|
|
425
|
+
- Hand-build full vendor graphs in TypeScript when a template + `duplicateFlow` exists
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## Controllers that commonly fire events
|
|
430
|
+
|
|
431
|
+
Examples in Tempo: `Site`, `Vendor`, `Member`, `Package`, `MemberAccounts`, `EmailList`, `PageTemplate`, `Extra`, `CloudFrontFlow`, mobile `Subscriptions`, `helpers/Site.ts`. Legacy Harmony PHP jobs may also call `EventHq/add` (e.g. billing).
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## Related Cursor rules
|
|
436
|
+
|
|
437
|
+
Project conventions for flows are also summarized in `.cursorrules` under **Flows (Workflow Engine)**. Keep this document and those rules aligned when changing the event pipeline or `twf_*` contracts.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* @author Favio Figueroa
|
|
10
10
|
*
|
|
11
11
|
* Created at : 2021-07-08 12:55:38
|
|
12
|
-
* Last modified :
|
|
12
|
+
* Last modified : 2026-11-06 — Rensi Arteaga — @ApiDoc for OpenAPI (spec 04)
|
|
13
13
|
*/
|
|
14
14
|
import { EntityManager } from 'typeorm';
|
|
15
15
|
import { Controller } from '@pxp-nd/core';
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* @author Favio Figueroa
|
|
11
11
|
*
|
|
12
12
|
* Created at : 2021-07-08 12:55:38
|
|
13
|
-
* Last modified :
|
|
13
|
+
* Last modified : 2026-11-06 — Rensi Arteaga — @ApiDoc for OpenAPI (spec 04)
|
|
14
14
|
*/
|
|
15
15
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
16
16
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
@@ -30,9 +30,13 @@ const core_1 = require("@pxp-nd/core");
|
|
|
30
30
|
const Action_1 = __importDefault(require("../entity/Action"));
|
|
31
31
|
const FieldMap_1 = __importDefault(require("../entity/FieldMap"));
|
|
32
32
|
const OriginName_1 = __importDefault(require("../entity/OriginName"));
|
|
33
|
+
const FlowNdApiDocs_1 = require("../helpers/FlowNdApiDocs");
|
|
33
34
|
let Action = class Action extends core_1.Controller {
|
|
34
35
|
async getFieldMapByAction(params, manager) {
|
|
35
36
|
const { originName } = params;
|
|
37
|
+
if (originName == null || String(originName).trim() === '') {
|
|
38
|
+
throw new core_1.PxpError(400, 'originName is required');
|
|
39
|
+
}
|
|
36
40
|
const fieldMapData = await (0, core_1.__)(manager.createQueryBuilder(FieldMap_1.default, 'fm')
|
|
37
41
|
.innerJoin(OriginName_1.default, 'on', 'on.originNameId = fm.originNameId')
|
|
38
42
|
.where("on.name = :n ", { n: originName })
|
|
@@ -61,8 +65,9 @@ let Action = class Action extends core_1.Controller {
|
|
|
61
65
|
__decorate([
|
|
62
66
|
(0, core_1.Get)(),
|
|
63
67
|
(0, core_1.DbSettings)('Orm'),
|
|
64
|
-
(0, core_1.ReadOnly)(
|
|
68
|
+
(0, core_1.ReadOnly)(true),
|
|
65
69
|
(0, core_1.Log)(true),
|
|
70
|
+
(0, core_1.ApiDoc)(FlowNdApiDocs_1.actionGetFieldMapByActionDoc),
|
|
66
71
|
__metadata("design:type", Function),
|
|
67
72
|
__metadata("design:paramtypes", [Object, typeorm_1.EntityManager]),
|
|
68
73
|
__metadata("design:returntype", Promise)
|
|
@@ -72,6 +77,7 @@ __decorate([
|
|
|
72
77
|
(0, core_1.DbSettings)('Orm'),
|
|
73
78
|
(0, core_1.ReadOnly)(true),
|
|
74
79
|
(0, core_1.Log)(true),
|
|
80
|
+
(0, core_1.ApiDoc)(FlowNdApiDocs_1.actionGetActionsDoc),
|
|
75
81
|
__metadata("design:type", Function),
|
|
76
82
|
__metadata("design:paramtypes", [Object, typeorm_1.EntityManager]),
|
|
77
83
|
__metadata("design:returntype", Promise)
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* 28-Sep-2023 SP06OCT23 Mercedes Zambrana Add Validation when change off status (validateFlow, deleteflow, saveFlow, saveflowName)
|
|
20
20
|
* 07-Feb-2025 8353064698 Mercedes Zambrana Add validation for opt in page when flow si funnel change
|
|
21
21
|
* 18-Feb-2025 8501163144 Mercedes Zambrana Add publish filter
|
|
22
|
+
* 06-Nov-2026 Rensi Arteaga @ApiDoc for OpenAPI (spec 04)
|
|
22
23
|
* ******************************************************************************
|
|
23
24
|
*/
|
|
24
25
|
import { EntityManager } from 'typeorm';
|
package/dist/controllers/Flow.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* 28-Sep-2023 SP06OCT23 Mercedes Zambrana Add Validation when change off status (validateFlow, deleteflow, saveFlow, saveflowName)
|
|
21
21
|
* 07-Feb-2025 8353064698 Mercedes Zambrana Add validation for opt in page when flow si funnel change
|
|
22
22
|
* 18-Feb-2025 8501163144 Mercedes Zambrana Add publish filter
|
|
23
|
+
* 06-Nov-2026 Rensi Arteaga @ApiDoc for OpenAPI (spec 04)
|
|
23
24
|
* ******************************************************************************
|
|
24
25
|
*/
|
|
25
26
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
@@ -69,8 +70,13 @@ const FlowInstance_1 = __importDefault(require("../entity/FlowInstance"));
|
|
|
69
70
|
const Node_2 = __importDefault(require("./Node"));
|
|
70
71
|
const axios_1 = __importDefault(require("axios"));
|
|
71
72
|
//import {configFacebookStrategy} from "./passport-facebook";
|
|
73
|
+
const FlowNdApiDocs_1 = require("../helpers/FlowNdApiDocs");
|
|
72
74
|
let Flow = class Flow extends core_1.Controller {
|
|
73
75
|
async get(params) {
|
|
76
|
+
const fid = Number(params.flowId);
|
|
77
|
+
if (!Number.isInteger(fid) || fid < 1) {
|
|
78
|
+
throw new core_1.PxpError(400, 'flowId is required and must be a positive integer');
|
|
79
|
+
}
|
|
74
80
|
const nodes = await Node_1.default.find({ flowId: params.flowId });
|
|
75
81
|
const restNodes = [];
|
|
76
82
|
for (let node of nodes) {
|
|
@@ -477,6 +483,10 @@ let Flow = class Flow extends core_1.Controller {
|
|
|
477
483
|
}
|
|
478
484
|
async getFlowRender(params) {
|
|
479
485
|
var _a;
|
|
486
|
+
const fid = Number(params.flowId);
|
|
487
|
+
if (!Number.isInteger(fid) || fid < 1) {
|
|
488
|
+
throw new core_1.PxpError(400, 'flowId is required and must be a positive integer');
|
|
489
|
+
}
|
|
480
490
|
const flow = await (0, typeorm_1.getManager)().findOne(Flow_1.default, params.flowId);
|
|
481
491
|
const totalNodes = await (0, typeorm_1.getManager)()
|
|
482
492
|
.createQueryBuilder(Node_1.default, "n")
|
|
@@ -782,6 +792,15 @@ let Flow = class Flow extends core_1.Controller {
|
|
|
782
792
|
const filter = (params === null || params === void 0 ? void 0 : params.filter) ? JSON.parse(params === null || params === void 0 ? void 0 : params.filter) : null;
|
|
783
793
|
const filters = (filter === null || filter === void 0 ? void 0 : filter.items) || [];
|
|
784
794
|
const logicOperator = ((_a = filter === null || filter === void 0 ? void 0 : filter.logicOperator) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || 'AND';
|
|
795
|
+
const allowedSort = ['flowId', 'name', 'createdAt', 'modifiedAt'];
|
|
796
|
+
const allowedDir = ['ASC', 'DESC'];
|
|
797
|
+
const sortCol = params.sort != null && params.sort !== '' ? String(params.sort) : 'flowId';
|
|
798
|
+
const dirVal = params.dir != null && params.dir !== ''
|
|
799
|
+
? String(params.dir).toUpperCase()
|
|
800
|
+
: 'ASC';
|
|
801
|
+
if (!allowedSort.includes(sortCol) || !allowedDir.includes(dirVal)) {
|
|
802
|
+
throw new core_1.PxpError(400, 'sort and dir must be allowed values (whitelist for SQL orderBy)');
|
|
803
|
+
}
|
|
785
804
|
const queryBuilder = await (0, typeorm_1.getManager)()
|
|
786
805
|
.createQueryBuilder()
|
|
787
806
|
.select([
|
|
@@ -888,7 +907,7 @@ let Flow = class Flow extends core_1.Controller {
|
|
|
888
907
|
// Sorting and pagination
|
|
889
908
|
//queryBuilder.orderBy(`f.${params.sort}`, params.dir).skip(params.start).take(params.limit);
|
|
890
909
|
// Sorting and pagination
|
|
891
|
-
queryBuilder.orderBy(`f.${
|
|
910
|
+
queryBuilder.orderBy(`f.${sortCol}`, dirVal);
|
|
892
911
|
// Agrega manualmente la cláusula LIMIT y OFFSET
|
|
893
912
|
queryBuilder.limit(params.limit).offset(params.start);
|
|
894
913
|
const data = await queryBuilder.getRawMany();
|
|
@@ -929,6 +948,15 @@ let Flow = class Flow extends core_1.Controller {
|
|
|
929
948
|
const filters = (filter === null || filter === void 0 ? void 0 : filter.items) || [];
|
|
930
949
|
const logicOperator = ((_a = filter === null || filter === void 0 ? void 0 : filter.logicOperator) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || "AND";
|
|
931
950
|
const publish = params._publish;
|
|
951
|
+
const allowedSort = ["flowId", "name", "createdAt", "modifiedAt"];
|
|
952
|
+
const allowedDir = ["ASC", "DESC"];
|
|
953
|
+
const sortCol = params.sort != null && params.sort !== "" ? String(params.sort) : "flowId";
|
|
954
|
+
const dirVal = params.dir != null && params.dir !== ""
|
|
955
|
+
? String(params.dir).toUpperCase()
|
|
956
|
+
: "ASC";
|
|
957
|
+
if (!allowedSort.includes(sortCol) || !allowedDir.includes(dirVal)) {
|
|
958
|
+
throw new core_1.PxpError(400, "sort and dir must be allowed values (whitelist for SQL orderBy)");
|
|
959
|
+
}
|
|
932
960
|
const queryBuilder = await (0, typeorm_1.getManager)()
|
|
933
961
|
.createQueryBuilder()
|
|
934
962
|
.select([
|
|
@@ -1040,7 +1068,7 @@ let Flow = class Flow extends core_1.Controller {
|
|
|
1040
1068
|
"f.status",
|
|
1041
1069
|
]);
|
|
1042
1070
|
// Ordenamiento y paginación
|
|
1043
|
-
queryBuilder.orderBy(`f.${
|
|
1071
|
+
queryBuilder.orderBy(`f.${sortCol}`, dirVal);
|
|
1044
1072
|
queryBuilder.limit(params.limit || 50).offset(params.start || 0);
|
|
1045
1073
|
// Obtener los datos
|
|
1046
1074
|
const data = await queryBuilder.getRawMany();
|
|
@@ -1117,6 +1145,7 @@ __decorate([
|
|
|
1117
1145
|
(0, core_1.DbSettings)('Orm'),
|
|
1118
1146
|
(0, core_1.ReadOnly)(true),
|
|
1119
1147
|
(0, core_1.Log)(true),
|
|
1148
|
+
(0, core_1.ApiDoc)(FlowNdApiDocs_1.flowGetDoc),
|
|
1120
1149
|
__metadata("design:type", Function),
|
|
1121
1150
|
__metadata("design:paramtypes", [Object]),
|
|
1122
1151
|
__metadata("design:returntype", Promise)
|
|
@@ -1135,6 +1164,7 @@ __decorate([
|
|
|
1135
1164
|
(0, core_1.DbSettings)('Orm'),
|
|
1136
1165
|
(0, core_1.ReadOnly)(false),
|
|
1137
1166
|
(0, core_1.Log)(true),
|
|
1167
|
+
(0, core_1.ApiDoc)(FlowNdApiDocs_1.flowDeleteFlowDoc),
|
|
1138
1168
|
__metadata("design:type", Function),
|
|
1139
1169
|
__metadata("design:paramtypes", [Object, typeorm_1.EntityManager]),
|
|
1140
1170
|
__metadata("design:returntype", Promise)
|
|
@@ -1144,6 +1174,7 @@ __decorate([
|
|
|
1144
1174
|
(0, core_1.DbSettings)('Orm'),
|
|
1145
1175
|
(0, core_1.ReadOnly)(false),
|
|
1146
1176
|
(0, core_1.Log)(true),
|
|
1177
|
+
(0, core_1.ApiDoc)(FlowNdApiDocs_1.flowSaveFlowNameDoc),
|
|
1147
1178
|
__metadata("design:type", Function),
|
|
1148
1179
|
__metadata("design:paramtypes", [Object, typeorm_1.EntityManager]),
|
|
1149
1180
|
__metadata("design:returntype", Promise)
|
|
@@ -1162,6 +1193,7 @@ __decorate([
|
|
|
1162
1193
|
(0, core_1.DbSettings)('Orm'),
|
|
1163
1194
|
(0, core_1.ReadOnly)(false),
|
|
1164
1195
|
(0, core_1.Log)(true),
|
|
1196
|
+
(0, core_1.ApiDoc)(FlowNdApiDocs_1.flowSaveFlowDoc),
|
|
1165
1197
|
__metadata("design:type", Function),
|
|
1166
1198
|
__metadata("design:paramtypes", [Object, typeorm_1.EntityManager]),
|
|
1167
1199
|
__metadata("design:returntype", Promise)
|
|
@@ -1172,6 +1204,7 @@ __decorate([
|
|
|
1172
1204
|
(0, core_1.DbSettings)('Orm'),
|
|
1173
1205
|
(0, core_1.ReadOnly)(true),
|
|
1174
1206
|
(0, core_1.Log)(true),
|
|
1207
|
+
(0, core_1.ApiDoc)(FlowNdApiDocs_1.flowGetFlowRenderDoc),
|
|
1175
1208
|
__metadata("design:type", Function),
|
|
1176
1209
|
__metadata("design:paramtypes", [Object]),
|
|
1177
1210
|
__metadata("design:returntype", Promise)
|
|
@@ -1181,6 +1214,7 @@ __decorate([
|
|
|
1181
1214
|
(0, core_1.DbSettings)('Orm'),
|
|
1182
1215
|
(0, core_1.ReadOnly)(false),
|
|
1183
1216
|
(0, core_1.Log)(true),
|
|
1217
|
+
(0, core_1.ApiDoc)(FlowNdApiDocs_1.flowInsertEventFlowDoc),
|
|
1184
1218
|
__metadata("design:type", Function),
|
|
1185
1219
|
__metadata("design:paramtypes", [Object, typeorm_1.EntityManager]),
|
|
1186
1220
|
__metadata("design:returntype", Promise)
|
|
@@ -1199,6 +1233,7 @@ __decorate([
|
|
|
1199
1233
|
(0, core_1.DbSettings)('Orm'),
|
|
1200
1234
|
(0, core_1.ReadOnly)(false),
|
|
1201
1235
|
(0, core_1.Log)(true),
|
|
1236
|
+
(0, core_1.ApiDoc)(FlowNdApiDocs_1.flowChangeStatusFlowDoc),
|
|
1202
1237
|
__metadata("design:type", Function),
|
|
1203
1238
|
__metadata("design:paramtypes", [Object, typeorm_1.EntityManager]),
|
|
1204
1239
|
__metadata("design:returntype", Promise)
|
|
@@ -1217,6 +1252,7 @@ __decorate([
|
|
|
1217
1252
|
(0, core_1.DbSettings)("Orm"),
|
|
1218
1253
|
(0, core_1.ReadOnly)(true),
|
|
1219
1254
|
(0, core_1.Log)(true),
|
|
1255
|
+
(0, core_1.ApiDoc)(FlowNdApiDocs_1.flowListTemplatesDoc),
|
|
1220
1256
|
__metadata("design:type", Function),
|
|
1221
1257
|
__metadata("design:paramtypes", [Object]),
|
|
1222
1258
|
__metadata("design:returntype", Promise)
|
|
@@ -1231,6 +1267,7 @@ __decorate([
|
|
|
1231
1267
|
__metadata("design:returntype", Promise)
|
|
1232
1268
|
], Flow.prototype, "changePublishTemplate", null);
|
|
1233
1269
|
Flow = __decorate([
|
|
1270
|
+
(0, core_1.ApiDocFor)('add', FlowNdApiDocs_1.flowAddDoc),
|
|
1234
1271
|
(0, core_1.Model)('flow-nd/Flow')
|
|
1235
1272
|
], Flow);
|
|
1236
1273
|
exports.default = Flow;
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* Last modified :
|
|
14
14
|
* ISSUE DATE NAME DESCRIPTION
|
|
15
15
|
* 9141239986 16-May-2025 Mercedes Zambrana Save flagName and FlagValue
|
|
16
|
+
* EF-23 08-Oct-2026 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';
|