@hotmeshio/hotmesh 0.14.3 → 0.14.5
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/build/modules/enums.d.ts +6 -0
- package/build/modules/enums.js +8 -2
- package/build/package.json +4 -3
- package/build/services/activities/hook.d.ts +10 -1
- package/build/services/activities/hook.js +45 -6
- package/build/services/dba/index.d.ts +1 -0
- package/build/services/dba/index.js +20 -3
- package/build/services/durable/client.js +13 -3
- package/build/services/durable/handle.d.ts +8 -1
- package/build/services/durable/handle.js +9 -1
- package/build/services/durable/worker.js +4 -0
- package/build/services/durable/workflow/signal.d.ts +1 -1
- package/build/services/durable/workflow/signal.js +2 -1
- package/build/services/mapper/index.d.ts +57 -2
- package/build/services/mapper/index.js +57 -2
- package/build/services/pipe/index.d.ts +444 -10
- package/build/services/pipe/index.js +444 -10
- package/build/services/store/index.d.ts +15 -2
- package/build/services/store/providers/postgres/kvtables.d.ts +1 -0
- package/build/services/store/providers/postgres/kvtables.js +46 -1
- package/build/services/store/providers/postgres/postgres.d.ts +25 -2
- package/build/services/store/providers/postgres/postgres.js +121 -4
- package/build/services/stream/registry.d.ts +1 -0
- package/build/services/stream/registry.js +12 -8
- package/build/services/task/index.d.ts +4 -1
- package/build/services/task/index.js +34 -6
- package/build/services/worker/index.js +2 -0
- package/build/types/dba.d.ts +11 -0
- package/build/types/hotmesh.d.ts +8 -0
- package/package.json +4 -3
- package/vitest.config.mts +1 -1
|
@@ -5,7 +5,358 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.Pipe = void 0;
|
|
7
7
|
const functions_1 = __importDefault(require("./functions"));
|
|
8
|
+
/**
|
|
9
|
+
* A functional data-transformation pipeline that resolves expressions
|
|
10
|
+
* row-by-row against live job data.
|
|
11
|
+
*
|
|
12
|
+
* @remarks
|
|
13
|
+
* ## Overview
|
|
14
|
+
*
|
|
15
|
+
* Pipe is the engine behind HotMesh's `@pipe` syntax — a uniform,
|
|
16
|
+
* functional approach to data mapping and transformation. Every
|
|
17
|
+
* ECMAScript operation is expressed as a **function with inputs**,
|
|
18
|
+
* eliminating the syntactic variability of the language (ternaries,
|
|
19
|
+
* property access, instance methods) in favor of a single,
|
|
20
|
+
* composable pattern.
|
|
21
|
+
*
|
|
22
|
+
* ## Postfix notation (Reverse Polish Notation)
|
|
23
|
+
*
|
|
24
|
+
* If you've used an RPN calculator, `@pipe` will feel familiar.
|
|
25
|
+
* Operands come first, then the operator consumes them:
|
|
26
|
+
*
|
|
27
|
+
* ```
|
|
28
|
+
* RPN: 4 4 + → 8
|
|
29
|
+
*
|
|
30
|
+
* @pipe:
|
|
31
|
+
* - [4, 4] ← operands
|
|
32
|
+
* - ["{@math.add}"] ← operator → 8
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* That's the entire model. Operands are pushed, an operator pops
|
|
36
|
+
* them, and the result becomes an operand for the next row.
|
|
37
|
+
*
|
|
38
|
+
* ## How it works — row by row
|
|
39
|
+
*
|
|
40
|
+
* A pipe is an ordered array of **rows**. Each row is an array of
|
|
41
|
+
* **cells**. The key rule: **every resolved value on a row flows
|
|
42
|
+
* into cell 0 of the next row** (the operator). The operator
|
|
43
|
+
* consumes all upstream operands, resolves to a new value, and
|
|
44
|
+
* that result *becomes an operand*. Any remaining cells on the
|
|
45
|
+
* same row are resolved independently and appended as additional
|
|
46
|
+
* operands. Then the cycle repeats.
|
|
47
|
+
*
|
|
48
|
+
* ```
|
|
49
|
+
* @pipe:
|
|
50
|
+
* ┌──────────────────────────────────────┐
|
|
51
|
+
* │ Row 0: [operand, operand, operand] │ ← resolve all cells
|
|
52
|
+
* │ │ │ │ │
|
|
53
|
+
* │ └────────┼────────┘ │
|
|
54
|
+
* │ │ │
|
|
55
|
+
* ├─────────────────────┼────────────────┤
|
|
56
|
+
* │ ▼ │
|
|
57
|
+
* │ Row 1: [ OPERATOR , operand] │ ← ALL operands from Row 0
|
|
58
|
+
* │ ▲ receives all ▲ │ flow into cell 0;
|
|
59
|
+
* │ │ from above │ │ result becomes operand;
|
|
60
|
+
* │ └──────────────┘ │ remaining cells resolve
|
|
61
|
+
* │ │ │ │
|
|
62
|
+
* │ └────────────┘ │
|
|
63
|
+
* │ │ │
|
|
64
|
+
* ├─────────────────────┼────────────────┤
|
|
65
|
+
* │ ▼ │
|
|
66
|
+
* │ Row 2: [ OPERATOR , operand] │ ← same pattern repeats
|
|
67
|
+
* └──────────────────────────────────────┘
|
|
68
|
+
* ▼
|
|
69
|
+
* final value = cell 0 of last row
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* Step by step:
|
|
73
|
+
*
|
|
74
|
+
* 1. **Row 0** — Every cell is resolved independently (static
|
|
75
|
+
* literals, `{data.*}` references, or nullary functions like
|
|
76
|
+
* `{@date.now}`). The resolved values are all **operands**.
|
|
77
|
+
* 2. **Row 1** — Cell 0 is the **operator** (`{@domain.method}`).
|
|
78
|
+
* It receives *all* operands from Row 0 as its arguments and
|
|
79
|
+
* produces a result. That result replaces cell 0 — it is now
|
|
80
|
+
* an operand. Any remaining cells (cell 1, 2, ...) on this row
|
|
81
|
+
* are resolved independently and become additional operands.
|
|
82
|
+
* 3. **Row 2** — Cell 0 is the next operator. It receives *all*
|
|
83
|
+
* operands from Row 1 (the prior result + any extra cells).
|
|
84
|
+
* The cycle repeats.
|
|
85
|
+
* 4. **Return** — The first cell of the final row is the result.
|
|
86
|
+
*
|
|
87
|
+
* When an additional cell on an operator row needs computed (not
|
|
88
|
+
* just a static value or simple reference), use a **nested
|
|
89
|
+
* `@pipe`** (sub-pipe) to resolve it. See Example 4 below.
|
|
90
|
+
*
|
|
91
|
+
* ## Three types of cell values
|
|
92
|
+
*
|
|
93
|
+
* | Type | Syntax | Example |
|
|
94
|
+
* |------|--------|---------|
|
|
95
|
+
* | **Static** | literal | `42`, `"hello"`, `true` |
|
|
96
|
+
* | **Dynamic** | `{path}` | `{a.output.data.name}` |
|
|
97
|
+
* | **Function** | `{@domain.method}` | `{@string.concat}`, `{@math.add}` |
|
|
98
|
+
*
|
|
99
|
+
* ## Available function domains
|
|
100
|
+
*
|
|
101
|
+
* - **array** — `get`, `length`, `join`, `concat`, `push`, `indexOf`, ...
|
|
102
|
+
* - **bitwise** — `and`, `or`, `xor`, `not`, ...
|
|
103
|
+
* - **conditional** — `ternary`, `equality`, `nullish`, ...
|
|
104
|
+
* - **cron** — `nextDelay`
|
|
105
|
+
* - **date** — `now`, `toLocaleString`, `yyyymmdd`, ...
|
|
106
|
+
* - **json** — `parse`, `stringify`
|
|
107
|
+
* - **logical** — `and`, `or`, `not`
|
|
108
|
+
* - **math** — `add`, `multiply`, `pow`, `max`, `min`, `abs`, ...
|
|
109
|
+
* - **number** — `isEven`, `isOdd`, `gte`, `lte`, ...
|
|
110
|
+
* - **object** — `create`, `get`, `set`, `keys`, ...
|
|
111
|
+
* - **string** — `concat`, `split`, `charAt`, `toLowerCase`, `includes`, ...
|
|
112
|
+
* - **symbol**
|
|
113
|
+
* - **unary**
|
|
114
|
+
*
|
|
115
|
+
* ---
|
|
116
|
+
*
|
|
117
|
+
* ## Example 1 — Simple field mapping (YAML)
|
|
118
|
+
*
|
|
119
|
+
* Most fields need only a one-to-one reference. Curly braces pull
|
|
120
|
+
* values from upstream activity outputs:
|
|
121
|
+
*
|
|
122
|
+
* ```yaml
|
|
123
|
+
* # Map fields from activities a and b into a new shape
|
|
124
|
+
* maps:
|
|
125
|
+
* first: "{a.output.data.first_name}"
|
|
126
|
+
* last: "{a.output.data.last_name}"
|
|
127
|
+
* email: "{b.output.data.email}"
|
|
128
|
+
* age: "{b.output.data.age}"
|
|
129
|
+
* company: "ACME Corp" # static string
|
|
130
|
+
* bonus: 500 # static number
|
|
131
|
+
* ```
|
|
132
|
+
*
|
|
133
|
+
* The equivalent JavaScript object passed to the mapper:
|
|
134
|
+
*
|
|
135
|
+
* ```typescript
|
|
136
|
+
* const rules = {
|
|
137
|
+
* first: '{a.output.data.first_name}',
|
|
138
|
+
* last: '{a.output.data.last_name}',
|
|
139
|
+
* email: '{b.output.data.email}',
|
|
140
|
+
* age: '{b.output.data.age}',
|
|
141
|
+
* company: 'ACME Corp',
|
|
142
|
+
* bonus: 500,
|
|
143
|
+
* };
|
|
144
|
+
* ```
|
|
145
|
+
*
|
|
146
|
+
* ## Example 2 — String transformation (YAML → JS)
|
|
147
|
+
*
|
|
148
|
+
* Build a `user_name` like `jdoe` from "John Doe". Follow the
|
|
149
|
+
* RPN flow — operands feed into the operator on the next row,
|
|
150
|
+
* the result becomes an operand, extra cells append more operands:
|
|
151
|
+
*
|
|
152
|
+
* ```yaml
|
|
153
|
+
* user_name:
|
|
154
|
+
* "@pipe":
|
|
155
|
+
* - ["{a.output.data.first_name}", 0]
|
|
156
|
+
* - ["{@string.charAt}", "{a.output.data.last_name}"]
|
|
157
|
+
* - ["{@string.concat}"]
|
|
158
|
+
* - ["{@string.toLowerCase}"]
|
|
159
|
+
* ```
|
|
160
|
+
*
|
|
161
|
+
* ```typescript
|
|
162
|
+
* // Identical logic in JavaScript
|
|
163
|
+
* const rules = {
|
|
164
|
+
* user_name: {
|
|
165
|
+
* '@pipe': [
|
|
166
|
+
* ['{a.output.data.first_name}', 0],
|
|
167
|
+
* ['{@string.charAt}', '{a.output.data.last_name}'],
|
|
168
|
+
* ['{@string.concat}'],
|
|
169
|
+
* ['{@string.toLowerCase}'],
|
|
170
|
+
* ],
|
|
171
|
+
* },
|
|
172
|
+
* };
|
|
173
|
+
* ```
|
|
174
|
+
*
|
|
175
|
+
* RPN trace (given first_name="John", last_name="Doe"):
|
|
176
|
+
*
|
|
177
|
+
* ```
|
|
178
|
+
* Row 0: ["John", 0] ← two operands
|
|
179
|
+
* Row 1: charAt("John", 0)="J" ← operator consumes both → result "J"
|
|
180
|
+
* then resolve "Doe" ← extra cell → operands are ["J", "Doe"]
|
|
181
|
+
* Row 2: concat("J", "Doe") ← operator → result "JDoe"
|
|
182
|
+
* operands are ["JDoe"]
|
|
183
|
+
* Row 3: toLowerCase("JDoe") ← operator → result "jdoe"
|
|
184
|
+
* ```
|
|
185
|
+
*
|
|
186
|
+
* ## Example 3 — Conditional logic (YAML → JS)
|
|
187
|
+
*
|
|
188
|
+
* Classify an employee as "Senior" or "Junior" based on age:
|
|
189
|
+
*
|
|
190
|
+
* ```yaml
|
|
191
|
+
* status:
|
|
192
|
+
* "@pipe":
|
|
193
|
+
* - ["{b.output.data.age}", 40]
|
|
194
|
+
* - ["{@number.gte}", "Senior", "Junior"]
|
|
195
|
+
* - ["{@conditional.ternary}"]
|
|
196
|
+
* ```
|
|
197
|
+
*
|
|
198
|
+
* ```typescript
|
|
199
|
+
* const rules = {
|
|
200
|
+
* status: {
|
|
201
|
+
* '@pipe': [
|
|
202
|
+
* ['{b.output.data.age}', 40],
|
|
203
|
+
* ['{@number.gte}', 'Senior', 'Junior'],
|
|
204
|
+
* ['{@conditional.ternary}'],
|
|
205
|
+
* ],
|
|
206
|
+
* },
|
|
207
|
+
* };
|
|
208
|
+
* ```
|
|
209
|
+
*
|
|
210
|
+
* RPN trace (given age=30):
|
|
211
|
+
*
|
|
212
|
+
* ```
|
|
213
|
+
* Row 0: [30, 40] ← two operands
|
|
214
|
+
* Row 1: gte(30, 40)=false ← operator consumes both → false
|
|
215
|
+
* resolve "Senior", "Junior" ← extra cells → [false, "Senior", "Junior"]
|
|
216
|
+
* Row 2: ternary(false, "Senior", "Junior") ← operator → "Junior"
|
|
217
|
+
* ```
|
|
218
|
+
*
|
|
219
|
+
* ## Example 4 — Nested pipes (fan-out / fan-in)
|
|
220
|
+
*
|
|
221
|
+
* Extract initials from a full name. Each nested `@pipe` runs
|
|
222
|
+
* independently (fan-out), then the first standard row after them
|
|
223
|
+
* receives all sub-pipe results as inputs (fan-in):
|
|
224
|
+
*
|
|
225
|
+
* ```yaml
|
|
226
|
+
* initials:
|
|
227
|
+
* "@pipe":
|
|
228
|
+
* - ["{a.output.data.full_name}", " "]
|
|
229
|
+
* - "@pipe": # fan-out: first initial
|
|
230
|
+
* - ["{@string.split}", 0]
|
|
231
|
+
* - ["{@array.get}", 0]
|
|
232
|
+
* - ["{@string.charAt}"]
|
|
233
|
+
* - "@pipe": # fan-out: last initial
|
|
234
|
+
* - ["{@string.split}", 1]
|
|
235
|
+
* - ["{@array.get}", 0]
|
|
236
|
+
* - ["{@string.charAt}"]
|
|
237
|
+
* - ["{@string.concat}"] # fan-in: combine both
|
|
238
|
+
* ```
|
|
239
|
+
*
|
|
240
|
+
* ```typescript
|
|
241
|
+
* const rules = {
|
|
242
|
+
* initials: {
|
|
243
|
+
* '@pipe': [
|
|
244
|
+
* ['{a.output.data.full_name}', ' '],
|
|
245
|
+
* { '@pipe': [['{@string.split}', 0], ['{@array.get}', 0], ['{@string.charAt}']] },
|
|
246
|
+
* { '@pipe': [['{@string.split}', 1], ['{@array.get}', 0], ['{@string.charAt}']] },
|
|
247
|
+
* ['{@string.concat}'],
|
|
248
|
+
* ],
|
|
249
|
+
* },
|
|
250
|
+
* };
|
|
251
|
+
* // "Luke Birdeau" → split → ["Luke","Birdeau"]
|
|
252
|
+
* // sub-pipe 1: get(0) → "Luke" → charAt(0) → "L"
|
|
253
|
+
* // sub-pipe 2: get(1) → "Birdeau" → charAt(0) → "B"
|
|
254
|
+
* // fan-in: concat("L", "B") → "LB"
|
|
255
|
+
* ```
|
|
256
|
+
*
|
|
257
|
+
* ## Example 5 — Reduce (iterate and accumulate)
|
|
258
|
+
*
|
|
259
|
+
* Transform an array of `{ full_name }` objects into
|
|
260
|
+
* `{ first, last }` pairs using `@reduce`. Context variables
|
|
261
|
+
* `{$item}`, `{$key}`, `{$index}`, `{$input}`, and `{$output}`
|
|
262
|
+
* are available inside the reducer body:
|
|
263
|
+
*
|
|
264
|
+
* ```typescript
|
|
265
|
+
* const jobData = {
|
|
266
|
+
* a: { output: { data: [
|
|
267
|
+
* { full_name: 'Luke Birdeau' },
|
|
268
|
+
* { full_name: 'John Doe' },
|
|
269
|
+
* ]}}
|
|
270
|
+
* };
|
|
271
|
+
*
|
|
272
|
+
* const rules = [
|
|
273
|
+
* ['{a.output.data}', []], // input array + initial accumulator
|
|
274
|
+
* { '@reduce': [
|
|
275
|
+
* { '@pipe': [['{$output}']] }, // carry forward accumulator
|
|
276
|
+
* { '@pipe': [
|
|
277
|
+
* { '@pipe': [['first']] },
|
|
278
|
+
* { '@pipe': [
|
|
279
|
+
* ['{$item.full_name}', ' '],
|
|
280
|
+
* ['{@string.split}', 0],
|
|
281
|
+
* ['{@array.get}'],
|
|
282
|
+
* ]},
|
|
283
|
+
* { '@pipe': [['last']] },
|
|
284
|
+
* { '@pipe': [
|
|
285
|
+
* ['{$item.full_name}', ' '],
|
|
286
|
+
* ['{@string.split}', 1],
|
|
287
|
+
* ['{@array.get}'],
|
|
288
|
+
* ]},
|
|
289
|
+
* ['{@object.create}'],
|
|
290
|
+
* ]},
|
|
291
|
+
* ['{@array.push}'],
|
|
292
|
+
* ]},
|
|
293
|
+
* ];
|
|
294
|
+
* // => [{ first: 'Luke', last: 'Birdeau' }, { first: 'John', last: 'Doe' }]
|
|
295
|
+
* ```
|
|
296
|
+
*
|
|
297
|
+
* ## Example 6 — Transition conditions
|
|
298
|
+
*
|
|
299
|
+
* Pipes power conditional transitions between workflow activities.
|
|
300
|
+
* In YAML, a transition fires only when the `@pipe` resolves to the
|
|
301
|
+
* `expected` value:
|
|
302
|
+
*
|
|
303
|
+
* ```yaml
|
|
304
|
+
* transitions:
|
|
305
|
+
* t1:
|
|
306
|
+
* - to: a1
|
|
307
|
+
* conditions:
|
|
308
|
+
* match:
|
|
309
|
+
* - expected: false
|
|
310
|
+
* actual:
|
|
311
|
+
* "@pipe":
|
|
312
|
+
* - ["{t1.output.data.a}", "goodbye"]
|
|
313
|
+
* - ["{@conditional.equality}"]
|
|
314
|
+
* ```
|
|
315
|
+
*
|
|
316
|
+
* ## Example 7 — Inline YAML with HotMesh.deploy
|
|
317
|
+
*
|
|
318
|
+
* HotMesh ships with an inline YAML parser, so a complete
|
|
319
|
+
* workflow with data mapping can be deployed in a single call:
|
|
320
|
+
*
|
|
321
|
+
* ```typescript
|
|
322
|
+
* await hotMesh.deploy(`
|
|
323
|
+
* app:
|
|
324
|
+
* id: myapp
|
|
325
|
+
* version: '1'
|
|
326
|
+
* graphs:
|
|
327
|
+
* - subscribes: order.process
|
|
328
|
+
* activities:
|
|
329
|
+
* t1:
|
|
330
|
+
* type: trigger
|
|
331
|
+
* a1:
|
|
332
|
+
* type: worker
|
|
333
|
+
* topic: inventory.check
|
|
334
|
+
* input:
|
|
335
|
+
* maps:
|
|
336
|
+
* itemId: "{t1.output.data.itemId}"
|
|
337
|
+
* output:
|
|
338
|
+
* schema:
|
|
339
|
+
* type: object
|
|
340
|
+
* job:
|
|
341
|
+
* maps:
|
|
342
|
+
* result: "{$self.output.data.available}"
|
|
343
|
+
* transitions:
|
|
344
|
+
* t1:
|
|
345
|
+
* - to: a1
|
|
346
|
+
* `);
|
|
347
|
+
*
|
|
348
|
+
* await hotMesh.activate('1');
|
|
349
|
+
* const response = await hotMesh.pubsub('order.process', { itemId: 'sku-42' });
|
|
350
|
+
* ```
|
|
351
|
+
*/
|
|
8
352
|
class Pipe {
|
|
353
|
+
/**
|
|
354
|
+
* @param rules - The ordered row array defining the pipeline.
|
|
355
|
+
* @param jobData - The current job data used to resolve `{data.*}`
|
|
356
|
+
* and `{activity.*}` references.
|
|
357
|
+
* @param context - Optional iteration context (`$item`, `$key`,
|
|
358
|
+
* `$output`, etc.) supplied during `@reduce` execution.
|
|
359
|
+
*/
|
|
9
360
|
constructor(rules, jobData, context) {
|
|
10
361
|
this.rules = rules;
|
|
11
362
|
this.jobData = jobData;
|
|
@@ -17,12 +368,32 @@ class Pipe {
|
|
|
17
368
|
isreduceType(currentRow) {
|
|
18
369
|
return !Array.isArray(currentRow) && '@reduce' in currentRow;
|
|
19
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* Returns `true` if the value is a `@pipe` object (i.e. `{ '@pipe': [...] }`).
|
|
373
|
+
*
|
|
374
|
+
* @param obj - The value to test.
|
|
375
|
+
*/
|
|
20
376
|
static isPipeObject(obj) {
|
|
21
377
|
return (typeof obj === 'object' &&
|
|
22
378
|
obj !== null &&
|
|
23
379
|
!Array.isArray(obj) &&
|
|
24
380
|
'@pipe' in obj);
|
|
25
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* One-shot convenience method that resolves a single value or
|
|
384
|
+
* `@pipe` expression against the given context.
|
|
385
|
+
*
|
|
386
|
+
* @param unresolved - A literal value, a `{data.*}` reference string,
|
|
387
|
+
* or a `@pipe` object.
|
|
388
|
+
* @param context - Partial {@link JobState} used for resolution.
|
|
389
|
+
* @returns The fully resolved value.
|
|
390
|
+
*
|
|
391
|
+
* @example
|
|
392
|
+
* ```typescript
|
|
393
|
+
* Pipe.resolve('{data.user.email}', jobState);
|
|
394
|
+
* Pipe.resolve({ '@pipe': [['{data.a}', '{data.b}'], ['{@math.multiply}']] }, jobState);
|
|
395
|
+
* ```
|
|
396
|
+
*/
|
|
26
397
|
static resolve(unresolved, context) {
|
|
27
398
|
let pipe;
|
|
28
399
|
if (Pipe.isPipeObject(unresolved)) {
|
|
@@ -34,8 +405,29 @@ class Pipe {
|
|
|
34
405
|
return pipe.process();
|
|
35
406
|
}
|
|
36
407
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
408
|
+
* Executes the pipeline row-by-row, resolving and transforming
|
|
409
|
+
* until the final value is produced.
|
|
410
|
+
*
|
|
411
|
+
* @remarks
|
|
412
|
+
* Row 0 is resolved independently (values only). Each subsequent
|
|
413
|
+
* row feeds the prior row's resolved output as arguments to the
|
|
414
|
+
* function named in cell 0. Nested `@pipe` rows are queued
|
|
415
|
+
* (fan-out) and collected into the next standard row (fan-in).
|
|
416
|
+
* `@reduce` rows iterate over the prior row's array output.
|
|
417
|
+
*
|
|
418
|
+
* @param resolved - Optional pre-resolved seed values (used
|
|
419
|
+
* internally by `reduce` to inject the accumulator).
|
|
420
|
+
* @returns The first cell of the final resolved row.
|
|
421
|
+
*
|
|
422
|
+
* @example
|
|
423
|
+
* ```typescript
|
|
424
|
+
* // Split a full name and grab the first element
|
|
425
|
+
* const pipe = new Pipe(
|
|
426
|
+
* [['{a.output.data.full_name}', ' '], ['{@string.split}', 0], ['{@array.get}']],
|
|
427
|
+
* { a: { output: { data: { full_name: 'Luke Birdeau' } } } },
|
|
428
|
+
* );
|
|
429
|
+
* pipe.process(); // => 'Luke'
|
|
430
|
+
* ```
|
|
39
431
|
*/
|
|
40
432
|
process(resolved = null) {
|
|
41
433
|
let index = 0;
|
|
@@ -74,14 +466,26 @@ class Pipe {
|
|
|
74
466
|
return clonedObj;
|
|
75
467
|
}
|
|
76
468
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
469
|
+
* Iterates over an array or object and accumulates a result,
|
|
470
|
+
* similar to `Array.prototype.reduce`.
|
|
471
|
+
*
|
|
472
|
+
* @remarks
|
|
473
|
+
* Inside the reducer body the following context variables are
|
|
474
|
+
* available as cell references:
|
|
475
|
+
*
|
|
476
|
+
* | Variable | Description |
|
|
477
|
+
* |----------|-------------|
|
|
478
|
+
* | `{$input}` | The full input collection |
|
|
479
|
+
* | `{$output}` | The current accumulator |
|
|
480
|
+
* | `{$item}` | The current element |
|
|
481
|
+
* | `{$key}` | The current key (string for objects, index string for arrays) |
|
|
482
|
+
* | `{$index}` | The current numeric index |
|
|
483
|
+
*
|
|
484
|
+
* The preceding row supplies `[inputCollection, initialAccumulator]`.
|
|
485
|
+
* If no initial accumulator is provided, it defaults to `null`.
|
|
486
|
+
*
|
|
487
|
+
* @param input - A two-element array: `[collection, initialAccumulator]`.
|
|
488
|
+
* @returns The final accumulated value, wrapped in an array.
|
|
85
489
|
* @private
|
|
86
490
|
*/
|
|
87
491
|
reduce(input) {
|
|
@@ -152,6 +556,14 @@ class Pipe {
|
|
|
152
556
|
}
|
|
153
557
|
}
|
|
154
558
|
}
|
|
559
|
+
/**
|
|
560
|
+
* Looks up a domain function by its `{@domain.method}` name string
|
|
561
|
+
* and returns the callable.
|
|
562
|
+
*
|
|
563
|
+
* @param functionName - A string like `{@math.add}` or `{@string.concat}`.
|
|
564
|
+
* @returns The resolved function reference.
|
|
565
|
+
* @throws If the domain or method is not registered.
|
|
566
|
+
*/
|
|
155
567
|
static resolveFunction(functionName) {
|
|
156
568
|
let [prefix, suffix] = functionName.split('.');
|
|
157
569
|
prefix = prefix.substring(2);
|
|
@@ -165,6 +577,14 @@ class Pipe {
|
|
|
165
577
|
}
|
|
166
578
|
return domain[suffix];
|
|
167
579
|
}
|
|
580
|
+
/**
|
|
581
|
+
* Resolves every cell in a single row independently — each cell
|
|
582
|
+
* is evaluated for function calls, context variables, or mappable
|
|
583
|
+
* references.
|
|
584
|
+
*
|
|
585
|
+
* @param cells - The array of {@link PipeItems} to resolve.
|
|
586
|
+
* @returns An array of resolved values, one per input cell.
|
|
587
|
+
*/
|
|
168
588
|
processCells(cells) {
|
|
169
589
|
const resolved = [];
|
|
170
590
|
if (Array.isArray(cells)) {
|
|
@@ -193,6 +613,14 @@ class Pipe {
|
|
|
193
613
|
currentCell.startsWith('{') &&
|
|
194
614
|
currentCell.endsWith('}'));
|
|
195
615
|
}
|
|
616
|
+
/**
|
|
617
|
+
* Resolves a single cell value by detecting its type — function
|
|
618
|
+
* call (`{@domain.fn}`), context variable (`{$item}`, `{$key}`,
|
|
619
|
+
* etc.), mappable reference (`{data.*}`), or literal.
|
|
620
|
+
*
|
|
621
|
+
* @param currentCell - The cell to resolve.
|
|
622
|
+
* @returns The resolved runtime value.
|
|
623
|
+
*/
|
|
196
624
|
resolveCellValue(currentCell) {
|
|
197
625
|
if (this.isFunction(currentCell)) {
|
|
198
626
|
const fn = Pipe.resolveFunction(currentCell);
|
|
@@ -221,6 +649,12 @@ class Pipe {
|
|
|
221
649
|
}
|
|
222
650
|
return current;
|
|
223
651
|
}
|
|
652
|
+
/**
|
|
653
|
+
* Resolves a `{data.*}` or `{activity.*}` reference by walking
|
|
654
|
+
* the dot-delimited path into {@link jobData}.
|
|
655
|
+
*
|
|
656
|
+
* @param currentCell - The mappable reference string (e.g. `{data.user.name}`).
|
|
657
|
+
*/
|
|
224
658
|
resolveMappableValue(currentCell) {
|
|
225
659
|
const term = this.resolveMapTerm(currentCell);
|
|
226
660
|
return this.getNestedProperty(this.jobData, term);
|
|
@@ -64,8 +64,21 @@ declare abstract class StoreService<Provider extends ProviderClient, Transaction
|
|
|
64
64
|
abstract setHookRules(hookRules: Record<string, HookRule[]>): Promise<any>;
|
|
65
65
|
abstract getHookRules(): Promise<Record<string, HookRule[]>>;
|
|
66
66
|
abstract getAllSymbols(): Promise<Symbols>;
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Leg1: Attempts to set the hook signal. If a pending signal occupies
|
|
69
|
+
* the key (race condition), overwrites it and returns the pending data.
|
|
70
|
+
* When called with a transaction, queues the setnxex (no pending detection).
|
|
71
|
+
*/
|
|
72
|
+
abstract setHookSignal(hook: HookSignal, transaction?: TransactionProvider): Promise<{
|
|
73
|
+
success: boolean;
|
|
74
|
+
pendingData?: string;
|
|
75
|
+
}>;
|
|
76
|
+
/**
|
|
77
|
+
* Leg2: Atomically gets the hook signal OR inserts a pending signal
|
|
78
|
+
* if no hook is registered yet (early signal). Returns the hook
|
|
79
|
+
* signal value, or undefined if we stored a pending signal instead.
|
|
80
|
+
*/
|
|
81
|
+
abstract getHookSignal(topic: string, resolved: string, pendingData?: string, pendingExpire?: number): Promise<string | undefined>;
|
|
69
82
|
abstract deleteHookSignal(topic: string, resolved: string): Promise<number | undefined>;
|
|
70
83
|
abstract addTaskQueues(keys: string[]): Promise<void>;
|
|
71
84
|
abstract getActiveTaskQueue(): Promise<string | null>;
|
|
@@ -10,6 +10,7 @@ export declare const KVTables: (context: PostgresStoreService) => {
|
|
|
10
10
|
hashStringToInt(str: string): number;
|
|
11
11
|
waitForTablesCreation(lockId: number, appName: string): Promise<void>;
|
|
12
12
|
checkIfTablesExist(client: PostgresClientType, appName: string): Promise<boolean>;
|
|
13
|
+
migrate(client: PostgresClientType | PostgresPoolClientType, appName: string): Promise<void>;
|
|
13
14
|
createTables(client: PostgresClientType | PostgresPoolClientType, appName: string): Promise<void>;
|
|
14
15
|
getTableNames(appName: string): string[];
|
|
15
16
|
getTableDefinitions(appName: string): Array<{
|
|
@@ -26,7 +26,8 @@ const KVTables = (context) => ({
|
|
|
26
26
|
// First, check if tables already exist (no lock needed)
|
|
27
27
|
const tablesExist = await this.checkIfTablesExist(client, appName);
|
|
28
28
|
if (tablesExist) {
|
|
29
|
-
// Tables
|
|
29
|
+
// Tables exist; apply any pending migrations
|
|
30
|
+
await this.migrate(client, appName);
|
|
30
31
|
return;
|
|
31
32
|
}
|
|
32
33
|
// Tables don't exist, need to acquire lock and create them
|
|
@@ -128,6 +129,31 @@ const KVTables = (context) => ({
|
|
|
128
129
|
const results = await Promise.all(checkTablePromises);
|
|
129
130
|
return results.every((res) => res.rows[0].table !== null);
|
|
130
131
|
},
|
|
132
|
+
async migrate(client, appName) {
|
|
133
|
+
const schemaName = context.storeClient.safeName(appName);
|
|
134
|
+
const jobsTable = `${schemaName}.jobs`;
|
|
135
|
+
// v0.14.5: track updated_at on job status changes
|
|
136
|
+
const { rows } = await client.query(`SELECT 1 FROM pg_trigger WHERE tgname = 'trg_update_jobs_updated_at' LIMIT 1`);
|
|
137
|
+
if (rows.length === 0) {
|
|
138
|
+
await client.query(`
|
|
139
|
+
CREATE OR REPLACE FUNCTION ${schemaName}.update_jobs_updated_at()
|
|
140
|
+
RETURNS TRIGGER AS $$
|
|
141
|
+
BEGIN
|
|
142
|
+
IF NEW.status <> OLD.status THEN
|
|
143
|
+
NEW.updated_at = NOW();
|
|
144
|
+
END IF;
|
|
145
|
+
RETURN NEW;
|
|
146
|
+
END;
|
|
147
|
+
$$ LANGUAGE plpgsql;
|
|
148
|
+
`);
|
|
149
|
+
await client.query(`
|
|
150
|
+
DROP TRIGGER IF EXISTS trg_update_jobs_updated_at ON ${jobsTable};
|
|
151
|
+
CREATE TRIGGER trg_update_jobs_updated_at
|
|
152
|
+
BEFORE UPDATE ON ${jobsTable}
|
|
153
|
+
FOR EACH ROW EXECUTE FUNCTION ${schemaName}.update_jobs_updated_at();
|
|
154
|
+
`);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
131
157
|
async createTables(client, appName) {
|
|
132
158
|
try {
|
|
133
159
|
await client.query('BEGIN');
|
|
@@ -302,6 +328,25 @@ const KVTables = (context) => ({
|
|
|
302
328
|
CREATE TRIGGER trg_enforce_live_job_uniqueness
|
|
303
329
|
BEFORE INSERT OR UPDATE ON ${fullTableName}
|
|
304
330
|
FOR EACH ROW EXECUTE PROCEDURE ${schemaName}.enforce_live_job_uniqueness();
|
|
331
|
+
`);
|
|
332
|
+
// Create function to update updated_at on status changes
|
|
333
|
+
await client.query(`
|
|
334
|
+
CREATE OR REPLACE FUNCTION ${schemaName}.update_jobs_updated_at()
|
|
335
|
+
RETURNS TRIGGER AS $$
|
|
336
|
+
BEGIN
|
|
337
|
+
IF NEW.status <> OLD.status THEN
|
|
338
|
+
NEW.updated_at = NOW();
|
|
339
|
+
END IF;
|
|
340
|
+
RETURN NEW;
|
|
341
|
+
END;
|
|
342
|
+
$$ LANGUAGE plpgsql;
|
|
343
|
+
`);
|
|
344
|
+
// Create trigger for updated_at on job status changes
|
|
345
|
+
await client.query(`
|
|
346
|
+
DROP TRIGGER IF EXISTS trg_update_jobs_updated_at ON ${fullTableName};
|
|
347
|
+
CREATE TRIGGER trg_update_jobs_updated_at
|
|
348
|
+
BEFORE UPDATE ON ${fullTableName}
|
|
349
|
+
FOR EACH ROW EXECUTE FUNCTION ${schemaName}.update_jobs_updated_at();
|
|
305
350
|
`);
|
|
306
351
|
// Create the attributes table with partitioning
|
|
307
352
|
const attributesTableName = `${fullTableName}_attributes`;
|
|
@@ -115,8 +115,31 @@ declare class PostgresStoreService extends StoreService<ProviderClient, Provider
|
|
|
115
115
|
getTransitions(appVersion: AppVID): Promise<Transitions>;
|
|
116
116
|
setHookRules(hookRules: Record<string, HookRule[]>): Promise<any>;
|
|
117
117
|
getHookRules(): Promise<Record<string, HookRule[]>>;
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Leg1: set hook signal, atomically detecting a pending signal.
|
|
120
|
+
*
|
|
121
|
+
* Standalone (no transaction): single CTE query that reads any existing
|
|
122
|
+
* pending value, then inserts the hook signal (overwriting pending or
|
|
123
|
+
* expired entries). Returns `{success, pendingData}` in one round trip.
|
|
124
|
+
*
|
|
125
|
+
* In a transaction: queues the setnxex; pending detection deferred.
|
|
126
|
+
*/
|
|
127
|
+
setHookSignal(hook: HookSignal, transaction?: ProviderTransaction): Promise<{
|
|
128
|
+
success: boolean;
|
|
129
|
+
pendingData?: string;
|
|
130
|
+
}>;
|
|
131
|
+
/**
|
|
132
|
+
* Leg2: get hook signal OR atomically set a pending signal.
|
|
133
|
+
*
|
|
134
|
+
* When `pendingData` is provided and no hook signal exists, the
|
|
135
|
+
* pending value is inserted in the SAME SQL statement — no second
|
|
136
|
+
* round trip. This is the transactional edge that prevents the
|
|
137
|
+
* signal from being lost: by the time the query returns, the
|
|
138
|
+
* pending key is already visible to leg1's setnxex.
|
|
139
|
+
*
|
|
140
|
+
* When `pendingData` is omitted, behaves as a plain read.
|
|
141
|
+
*/
|
|
142
|
+
getHookSignal(topic: string, resolved: string, pendingData?: string, pendingExpire?: number): Promise<string | undefined>;
|
|
120
143
|
deleteHookSignal(topic: string, resolved: string): Promise<number | undefined>;
|
|
121
144
|
addTaskQueues(keys: string[]): Promise<void>;
|
|
122
145
|
getActiveTaskQueue(): Promise<string | null>;
|