@hostwebhook/node-sdk 0.1.0
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/dist/code-runner.d.ts +20 -0
- package/dist/code-runner.js +138 -0
- package/dist/contratos.d.ts +121 -0
- package/dist/contratos.js +24 -0
- package/dist/dto/output-node.dto.d.ts +19 -0
- package/dist/dto/output-node.dto.js +96 -0
- package/dist/ensure-meta.d.ts +22 -0
- package/dist/ensure-meta.js +35 -0
- package/dist/execute-with-iteration.d.ts +18 -0
- package/dist/execute-with-iteration.js +66 -0
- package/dist/filter-utils.d.ts +22 -0
- package/dist/filter-utils.js +178 -0
- package/dist/handler-helpers.d.ts +21 -0
- package/dist/handler-helpers.js +53 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +73 -0
- package/dist/log-metadata.d.ts +191 -0
- package/dist/log-metadata.js +375 -0
- package/dist/node-dispatch.registry.d.ts +32 -0
- package/dist/node-dispatch.registry.js +45 -0
- package/dist/node-executors.d.ts +299 -0
- package/dist/node-executors.js +555 -0
- package/dist/node-lifecycle.d.ts +399 -0
- package/dist/node-lifecycle.js +782 -0
- package/dist/normalize-nodes.d.ts +18 -0
- package/dist/normalize-nodes.js +22 -0
- package/dist/output-node-ref.schema.d.ts +82 -0
- package/dist/output-node-ref.schema.js +90 -0
- package/dist/output-webhook-scope.d.ts +36 -0
- package/dist/output-webhook-scope.js +42 -0
- package/dist/payload-preview.d.ts +10 -0
- package/dist/payload-preview.js +39 -0
- package/dist/pipeline.constants.d.ts +29 -0
- package/dist/pipeline.constants.js +51 -0
- package/dist/pre-request-pool.d.ts +58 -0
- package/dist/pre-request-pool.js +308 -0
- package/dist/pre-request-runner-source.d.ts +28 -0
- package/dist/pre-request-runner-source.js +411 -0
- package/dist/regex-de-inquilino.d.ts +15 -0
- package/dist/regex-de-inquilino.js +98 -0
- package/dist/request-context.d.ts +18 -0
- package/dist/request-context.js +34 -0
- package/dist/retry-transient.d.ts +54 -0
- package/dist/retry-transient.js +67 -0
- package/dist/retry-utils.d.ts +17 -0
- package/dist/retry-utils.js +23 -0
- package/dist/schema-validator-utils.d.ts +9 -0
- package/dist/schema-validator-utils.js +140 -0
- package/dist/ssrf-guard.d.ts +202 -0
- package/dist/ssrf-guard.js +917 -0
- package/dist/swallow.d.ts +52 -0
- package/dist/swallow.js +55 -0
- package/dist/template-render.d.ts +33 -0
- package/dist/template-render.js +43 -0
- package/dist/try-parse.d.ts +41 -0
- package/dist/try-parse.js +69 -0
- package/dist/workspace-payloads.d.ts +66 -0
- package/dist/workspace-payloads.js +496 -0
- package/package.json +35 -0
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared node execution functions.
|
|
4
|
+
*
|
|
5
|
+
* Both pipeline-run.service.ts (Run Pipeline / test) and event-pipeline.service.ts
|
|
6
|
+
* (real events) call these functions so that "how to execute node type X" lives in
|
|
7
|
+
* exactly ONE place. Callers still own Delivery records, child events, telemetry,
|
|
8
|
+
* and output-routing — only core execution logic is here.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.HANDLERLESS_NODE_TYPES = void 0;
|
|
12
|
+
exports.executeFilter = executeFilter;
|
|
13
|
+
exports.executeTransform = executeTransform;
|
|
14
|
+
exports.executeHttpAction = executeHttpAction;
|
|
15
|
+
exports.executeEmailAction = executeEmailAction;
|
|
16
|
+
exports.executeSheetsAction = executeSheetsAction;
|
|
17
|
+
exports.executeDriveAction = executeDriveAction;
|
|
18
|
+
exports.executeVectorStoreNode = executeVectorStoreNode;
|
|
19
|
+
exports.executeFileTransformNode = executeFileTransformNode;
|
|
20
|
+
exports.executePostgresAction = executePostgresAction;
|
|
21
|
+
exports.executeCalendarAction = executeCalendarAction;
|
|
22
|
+
exports.executeDocsAction = executeDocsAction;
|
|
23
|
+
exports.executeMarkdownNode = executeMarkdownNode;
|
|
24
|
+
exports.executeAiNode = executeAiNode;
|
|
25
|
+
exports.executeMongoAction = executeMongoAction;
|
|
26
|
+
exports.executeAggregator = executeAggregator;
|
|
27
|
+
exports.executeCache = executeCache;
|
|
28
|
+
exports.executeRateLimiter = executeRateLimiter;
|
|
29
|
+
exports.executeConditional = executeConditional;
|
|
30
|
+
exports.executeSchemaValidator = executeSchemaValidator;
|
|
31
|
+
exports.executeNotificationAction = executeNotificationAction;
|
|
32
|
+
exports.executeSplit = executeSplit;
|
|
33
|
+
exports.executeCode = executeCode;
|
|
34
|
+
exports.executeLoopFanOut = executeLoopFanOut;
|
|
35
|
+
exports.isHandlerless = isHandlerless;
|
|
36
|
+
const common_1 = require("@nestjs/common");
|
|
37
|
+
const template_engine_1 = require("@hostwebhook/template-engine");
|
|
38
|
+
const schemaLogger = new common_1.Logger('SchemaValidator');
|
|
39
|
+
const filterLogger = new common_1.Logger('Filter');
|
|
40
|
+
const conditionalLogger = new common_1.Logger('Conditional');
|
|
41
|
+
const filter_utils_1 = require("./filter-utils");
|
|
42
|
+
const schema_validator_utils_1 = require("./schema-validator-utils");
|
|
43
|
+
const try_parse_1 = require("./try-parse");
|
|
44
|
+
const code_runner_1 = require("./code-runner");
|
|
45
|
+
function executeFilter(entity, payload, context) {
|
|
46
|
+
const filterCount = (entity.filters ?? []).filter((f) => f.enabled !== false).length;
|
|
47
|
+
const meta = payload._meta;
|
|
48
|
+
filterLogger.log(`"${entity.name}" evaluating — ${filterCount} active filters, mode=${entity.filterMode}, iterable=${!!meta?.iterable}${meta?.count ? `, ${meta.count} items` : ''}`);
|
|
49
|
+
const filterOpts = context?.workspacePayloads
|
|
50
|
+
? { workspacePayloads: context.workspacePayloads }
|
|
51
|
+
: undefined;
|
|
52
|
+
const start = Date.now();
|
|
53
|
+
let result;
|
|
54
|
+
try {
|
|
55
|
+
result = (0, filter_utils_1.evaluateFiltersIterable)(entity.filters, payload, entity.filterMode, entity.outputAsSingle, filterOpts);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
const ms = Date.now() - start;
|
|
59
|
+
common_1.Logger.error(`[Filter] "${entity.name}" CRASHED after ${ms}ms: ${err instanceof Error ? err.message : err}`, 'NodeExecutors');
|
|
60
|
+
return {
|
|
61
|
+
statusCode: 500,
|
|
62
|
+
responseBody: JSON.stringify({
|
|
63
|
+
error: err instanceof Error ? err.message : 'Filter evaluation failed',
|
|
64
|
+
}),
|
|
65
|
+
latencyMs: ms,
|
|
66
|
+
outputPayload: payload,
|
|
67
|
+
passed: false,
|
|
68
|
+
totalItems: 0,
|
|
69
|
+
passedItems: 0,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const { passed, resultPayload, totalItems, passedItems } = result;
|
|
73
|
+
const ms = Date.now() - start;
|
|
74
|
+
filterLogger.log(`"${entity.name}" ${passed ? 'PASSED' : 'BLOCKED'} — ${passedItems}/${totalItems} items in ${ms}ms`);
|
|
75
|
+
return {
|
|
76
|
+
statusCode: passed ? 200 : 403,
|
|
77
|
+
responseBody: JSON.stringify({
|
|
78
|
+
passed,
|
|
79
|
+
totalItems,
|
|
80
|
+
passedItems,
|
|
81
|
+
payload: resultPayload,
|
|
82
|
+
}),
|
|
83
|
+
latencyMs: ms,
|
|
84
|
+
outputPayload: resultPayload,
|
|
85
|
+
passed,
|
|
86
|
+
totalItems,
|
|
87
|
+
passedItems,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async function executeTransform(svc, entity, payload, context) {
|
|
91
|
+
return svc.executeSingle(entity, payload, {
|
|
92
|
+
headers: context?.headers,
|
|
93
|
+
meta: context?.eventMeta,
|
|
94
|
+
workspacePayloads: context?.workspacePayloads,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
async function executeHttpAction(svc, entity, payload, context) {
|
|
98
|
+
return svc.executeSingle(entity, payload, {
|
|
99
|
+
eventMeta: context?.eventMeta,
|
|
100
|
+
workspacePayloads: context?.workspacePayloads,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
async function executeEmailAction(svc, entity, payload) {
|
|
104
|
+
return svc.executeSingle(entity, payload);
|
|
105
|
+
}
|
|
106
|
+
async function executeSheetsAction(svc, entity, payload, context) {
|
|
107
|
+
const start = Date.now();
|
|
108
|
+
const result = await svc.executeForEvent(entity, payload, {
|
|
109
|
+
workspacePayloads: context?.workspacePayloads,
|
|
110
|
+
});
|
|
111
|
+
const statusCode = result?.statusCode ?? (result?.success ? 200 : 500);
|
|
112
|
+
const responseBody = typeof result?.responseBody === 'string'
|
|
113
|
+
? result.responseBody
|
|
114
|
+
: JSON.stringify(result ?? {});
|
|
115
|
+
return {
|
|
116
|
+
statusCode,
|
|
117
|
+
responseBody,
|
|
118
|
+
latencyMs: result?.latencyMs ?? Date.now() - start,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
async function executeDriveAction(svc, entity, payload, context) {
|
|
122
|
+
const start = Date.now();
|
|
123
|
+
const result = await svc.executeForEvent(entity, payload, {
|
|
124
|
+
workspacePayloads: context?.workspacePayloads,
|
|
125
|
+
eventMeta: context?.eventMeta,
|
|
126
|
+
});
|
|
127
|
+
const statusCode = result?.statusCode ?? (result?.success ? 200 : 500);
|
|
128
|
+
const responseBody = typeof result?.responseBody === 'string'
|
|
129
|
+
? result.responseBody
|
|
130
|
+
: JSON.stringify(result ?? {});
|
|
131
|
+
return {
|
|
132
|
+
statusCode,
|
|
133
|
+
responseBody,
|
|
134
|
+
latencyMs: result?.latencyMs ?? Date.now() - start,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
async function executeVectorStoreNode(svc, entity, payload, context) {
|
|
138
|
+
const start = Date.now();
|
|
139
|
+
// Wrapped-payload shape: the service builds ctx via { ...payload, meta }
|
|
140
|
+
// and renders templates against ctx.payload, so the iterated item must
|
|
141
|
+
// sit under `payload` — passing raw payload here would break {{payload.X}}.
|
|
142
|
+
const result = await svc.executeForEvent(entity, {
|
|
143
|
+
payload,
|
|
144
|
+
workspacePayloads: context?.workspacePayloads,
|
|
145
|
+
eventMeta: context?.eventMeta,
|
|
146
|
+
});
|
|
147
|
+
const statusCode = result?.statusCode ?? (result?.success ? 200 : 500);
|
|
148
|
+
const responseBody = typeof result?.responseBody === 'string'
|
|
149
|
+
? result.responseBody
|
|
150
|
+
: JSON.stringify(result ?? {});
|
|
151
|
+
return {
|
|
152
|
+
statusCode,
|
|
153
|
+
responseBody,
|
|
154
|
+
latencyMs: result?.latencyMs ?? Date.now() - start,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
async function executeFileTransformNode(svc, entity, payload, context) {
|
|
158
|
+
const start = Date.now();
|
|
159
|
+
const result = await svc.executeForEvent(entity, payload, {
|
|
160
|
+
workspacePayloads: context?.workspacePayloads,
|
|
161
|
+
});
|
|
162
|
+
const statusCode = result?.statusCode ?? (result?.success ? 200 : 500);
|
|
163
|
+
const responseBody = typeof result?.responseBody === 'string'
|
|
164
|
+
? result.responseBody
|
|
165
|
+
: JSON.stringify(result ?? {});
|
|
166
|
+
return {
|
|
167
|
+
statusCode,
|
|
168
|
+
responseBody,
|
|
169
|
+
latencyMs: result?.latencyMs ?? Date.now() - start,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
async function executePostgresAction(svc, entity, payload, context) {
|
|
173
|
+
const start = Date.now();
|
|
174
|
+
// Wrapped-payload shape — see executeVectorStoreNode for rationale.
|
|
175
|
+
const result = await svc.executeForEvent(entity, {
|
|
176
|
+
payload,
|
|
177
|
+
workspacePayloads: context?.workspacePayloads,
|
|
178
|
+
eventMeta: context?.eventMeta,
|
|
179
|
+
});
|
|
180
|
+
const statusCode = result?.statusCode ?? (result?.success ? 200 : 500);
|
|
181
|
+
const responseBody = typeof result?.responseBody === 'string'
|
|
182
|
+
? result.responseBody
|
|
183
|
+
: JSON.stringify(result ?? {});
|
|
184
|
+
return {
|
|
185
|
+
statusCode,
|
|
186
|
+
responseBody,
|
|
187
|
+
latencyMs: result?.latencyMs ?? Date.now() - start,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
async function executeCalendarAction(svc, entity, payload, context) {
|
|
191
|
+
const start = Date.now();
|
|
192
|
+
const result = await svc.executeForEvent(entity, payload, {
|
|
193
|
+
workspacePayloads: context?.workspacePayloads,
|
|
194
|
+
});
|
|
195
|
+
const statusCode = result?.statusCode ?? (result?.success ? 200 : 500);
|
|
196
|
+
const responseBody = typeof result?.responseBody === 'string'
|
|
197
|
+
? result.responseBody
|
|
198
|
+
: JSON.stringify(result ?? {});
|
|
199
|
+
return {
|
|
200
|
+
statusCode,
|
|
201
|
+
responseBody,
|
|
202
|
+
latencyMs: result?.latencyMs ?? Date.now() - start,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
async function executeDocsAction(svc, entity, payload, context) {
|
|
206
|
+
const start = Date.now();
|
|
207
|
+
const result = await svc.executeForEvent(entity, payload, {
|
|
208
|
+
workspacePayloads: context?.workspacePayloads,
|
|
209
|
+
});
|
|
210
|
+
const statusCode = result?.statusCode ?? (result?.success ? 200 : 500);
|
|
211
|
+
const responseBody = typeof result?.responseBody === 'string'
|
|
212
|
+
? result.responseBody
|
|
213
|
+
: JSON.stringify(result ?? {});
|
|
214
|
+
return {
|
|
215
|
+
statusCode,
|
|
216
|
+
responseBody,
|
|
217
|
+
latencyMs: result?.latencyMs ?? Date.now() - start,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
async function executeMarkdownNode(svc, entity, payload, context) {
|
|
221
|
+
const start = Date.now();
|
|
222
|
+
const result = await svc.executeForEvent(entity, payload, {
|
|
223
|
+
workspacePayloads: context?.workspacePayloads,
|
|
224
|
+
});
|
|
225
|
+
const statusCode = result?.statusCode ?? (result?.success ? 200 : 500);
|
|
226
|
+
const responseBody = typeof result?.responseBody === 'string'
|
|
227
|
+
? result.responseBody
|
|
228
|
+
: JSON.stringify(result ?? {});
|
|
229
|
+
return {
|
|
230
|
+
statusCode,
|
|
231
|
+
responseBody,
|
|
232
|
+
latencyMs: result?.latencyMs ?? Date.now() - start,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
async function executeAiNode(svc, entity, payload, context) {
|
|
236
|
+
const start = Date.now();
|
|
237
|
+
const result = await svc.executeForEvent(entity, payload, {
|
|
238
|
+
workspacePayloads: context?.workspacePayloads,
|
|
239
|
+
eventMeta: context?.eventMeta,
|
|
240
|
+
});
|
|
241
|
+
const statusCode = result?.statusCode ?? (result?.success ? 200 : 500);
|
|
242
|
+
const responseBody = typeof result?.responseBody === 'string'
|
|
243
|
+
? result.responseBody
|
|
244
|
+
: JSON.stringify(result ?? {});
|
|
245
|
+
return {
|
|
246
|
+
statusCode,
|
|
247
|
+
responseBody,
|
|
248
|
+
latencyMs: result?.latencyMs ?? Date.now() - start,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
async function executeMongoAction(svc, entity, payload, context) {
|
|
252
|
+
const res = await svc.executeForEvent(entity, {
|
|
253
|
+
payload,
|
|
254
|
+
workspacePayloads: context?.workspacePayloads,
|
|
255
|
+
});
|
|
256
|
+
return {
|
|
257
|
+
statusCode: 200,
|
|
258
|
+
responseBody: JSON.stringify(res.result ?? {}),
|
|
259
|
+
latencyMs: res.latencyMs,
|
|
260
|
+
outputPayload: res.result ?? {},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
async function executeAggregator(svc, entity, payload) {
|
|
264
|
+
return svc.processBatchPayload(entity, payload);
|
|
265
|
+
}
|
|
266
|
+
function executeCache(svc, entity, payload) {
|
|
267
|
+
const start = Date.now();
|
|
268
|
+
const { action, outputPayload } = svc.processEvent(entity, payload);
|
|
269
|
+
return {
|
|
270
|
+
statusCode: action === 'blocked' ? 304 : 200,
|
|
271
|
+
responseBody: JSON.stringify({ action, payload: outputPayload }),
|
|
272
|
+
latencyMs: Date.now() - start,
|
|
273
|
+
outputPayload: outputPayload ?? undefined,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function executeRateLimiter(svc, entity) {
|
|
277
|
+
const start = Date.now();
|
|
278
|
+
const { allowed, delayMs } = svc.checkRate(entity);
|
|
279
|
+
return {
|
|
280
|
+
statusCode: allowed ? 200 : 429,
|
|
281
|
+
responseBody: JSON.stringify({ allowed, delayMs }),
|
|
282
|
+
latencyMs: Date.now() - start,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function executeConditional(entity, payload, context) {
|
|
286
|
+
const start = Date.now();
|
|
287
|
+
const meta = payload?._meta;
|
|
288
|
+
conditionalLogger.log(`"${entity.name}" _meta=${JSON.stringify(meta)} iterateField=${meta?.iterateField} items=${Array.isArray(payload?.[meta?.iterateField]) ? payload[meta.iterateField].length : 'N/A'}`);
|
|
289
|
+
for (const branch of entity.branches ?? []) {
|
|
290
|
+
conditionalLogger.debug(`"${entity.name}" branch="${branch.name}" filters=${JSON.stringify(branch.filters)}`);
|
|
291
|
+
}
|
|
292
|
+
const filterOpts = context?.workspacePayloads
|
|
293
|
+
? { workspacePayloads: context.workspacePayloads }
|
|
294
|
+
: undefined;
|
|
295
|
+
let matched = null;
|
|
296
|
+
for (const branch of entity.branches ?? []) {
|
|
297
|
+
if ((branch.filters ?? []).length === 0)
|
|
298
|
+
continue;
|
|
299
|
+
const { passed } = (0, filter_utils_1.evaluateFiltersIterable)(branch.filters, payload, branch.filterMode ?? 'and', false, filterOpts);
|
|
300
|
+
conditionalLogger.log(`"${entity.name}" branch="${branch.name}" passed=${passed}`);
|
|
301
|
+
if (passed) {
|
|
302
|
+
matched = branch;
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
statusCode: 200,
|
|
308
|
+
responseBody: JSON.stringify({ matched: matched?.name ?? null }),
|
|
309
|
+
latencyMs: Date.now() - start,
|
|
310
|
+
outputPayload: payload,
|
|
311
|
+
matchedBranch: matched,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function executeSchemaValidator(entity, payload) {
|
|
315
|
+
const start = Date.now();
|
|
316
|
+
const result = (0, schema_validator_utils_1.validateSchema)(entity.fields, payload, entity.strictMode);
|
|
317
|
+
const latencyMs = Date.now() - start;
|
|
318
|
+
if (result.valid) {
|
|
319
|
+
schemaLogger.log(`Schema "${entity.name}" VALID — ${entity.fields?.length ?? 0} fields${entity.strictMode ? ' (strict)' : ''} (${latencyMs}ms)`);
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
schemaLogger.warn(`Schema "${entity.name}" INVALID — ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.message}`).join(', ')} (${latencyMs}ms)`);
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
statusCode: 200, // Always 200 — validity is in responseBody.valid, routing via getOutputNodes
|
|
326
|
+
responseBody: JSON.stringify(result),
|
|
327
|
+
latencyMs,
|
|
328
|
+
outputPayload: payload,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
async function executeNotificationAction(svc, entity, payload, notifCtx) {
|
|
332
|
+
const start = Date.now();
|
|
333
|
+
const result = await svc.sendForEvent(entity, notifCtx ?? {}, payload);
|
|
334
|
+
return {
|
|
335
|
+
statusCode: result.success ? 200 : (result.statusCode ?? 500),
|
|
336
|
+
responseBody: JSON.stringify(result),
|
|
337
|
+
latencyMs: Date.now() - start,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
/** Resolve a dot-notation path (e.g. "data.orders") from an object */
|
|
341
|
+
function getNestedValue(obj, path) {
|
|
342
|
+
let current = obj;
|
|
343
|
+
for (const key of path.split('.')) {
|
|
344
|
+
if (current == null || typeof current !== 'object')
|
|
345
|
+
return undefined;
|
|
346
|
+
current = current[key];
|
|
347
|
+
}
|
|
348
|
+
return current;
|
|
349
|
+
}
|
|
350
|
+
// resolveFieldPath replaced by resolveFieldPath from @hostwebhook/template-engine
|
|
351
|
+
function executeSplit(entity, payload) {
|
|
352
|
+
const start = Date.now();
|
|
353
|
+
// Unwrap iterable payload — split operates on the inner data, not the wrapper
|
|
354
|
+
let effective = payload;
|
|
355
|
+
const meta = payload._meta;
|
|
356
|
+
if (meta?.iterable && meta.iterateField) {
|
|
357
|
+
const arr = payload[meta.iterateField];
|
|
358
|
+
if (Array.isArray(arr) && arr.length > 0) {
|
|
359
|
+
effective =
|
|
360
|
+
typeof arr[0] === 'object' && arr[0] !== null
|
|
361
|
+
? { ...arr[0] }
|
|
362
|
+
: { value: arr[0] };
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const outputs = [];
|
|
366
|
+
const assignedFields = new Set();
|
|
367
|
+
for (const output of entity.outputs ?? []) {
|
|
368
|
+
const field = (0, template_engine_1.resolveFieldPath)(output.field);
|
|
369
|
+
const value = getNestedValue(effective, field);
|
|
370
|
+
assignedFields.add(field.split('.')[0]);
|
|
371
|
+
outputs.push({ name: output.name, field: output.field, payload: value });
|
|
372
|
+
}
|
|
373
|
+
// Build remainder: top-level fields not assigned to any output
|
|
374
|
+
const remainderEntries = Object.entries(effective).filter(([key]) => key !== '_meta' && !assignedFields.has(key));
|
|
375
|
+
const remainder = remainderEntries.length > 0 ? Object.fromEntries(remainderEntries) : null;
|
|
376
|
+
return {
|
|
377
|
+
statusCode: 200,
|
|
378
|
+
responseBody: JSON.stringify({ splitOutputs: outputs, remainder }),
|
|
379
|
+
latencyMs: Date.now() - start,
|
|
380
|
+
outputPayload: effective,
|
|
381
|
+
splitOutputs: outputs,
|
|
382
|
+
remainder,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Ejecutar el nodo de código.
|
|
387
|
+
*
|
|
388
|
+
* ## Por qué esto ya no corre aquí dentro
|
|
389
|
+
*
|
|
390
|
+
* Antes iba a `renderTemplate(..., { mode: 'code' })`, o sea a `node:vm` con
|
|
391
|
+
* una lista negra sobre el texto fuente. `vm` no es una frontera y una lista
|
|
392
|
+
* de expresiones regulares sobre el fuente no es un control: la concatenación
|
|
393
|
+
* de cadenas la cruza sin despeinarse, y el contexto se sembraba con los
|
|
394
|
+
* intrínsecos del propio realm, así que `Math.constructor.constructor` era el
|
|
395
|
+
* `Function` de la casa.
|
|
396
|
+
*
|
|
397
|
+
* Con eso, un `return Math["constr"+"uctor"]…("return this")()["proc"+"ess"].env`
|
|
398
|
+
* devolvía `process.env` — es decir `MONGO_URI` y `API_SECRET`, que es la
|
|
399
|
+
* entrada del PBKDF2 con el que se descifran las credenciales guardadas de
|
|
400
|
+
* TODOS los inquilinos. Y cambiando la pieza final, ejecución de comandos.
|
|
401
|
+
*
|
|
402
|
+
* Ahora corre en el mismo hijo desechable que los scripts pre-request: entorno
|
|
403
|
+
* vacío, sin ficheros ni spawn donde el runtime lo soporte, muerto tras cada
|
|
404
|
+
* ejecución. Salirse del vm sigue siendo fácil; lo que cambia es que ya no
|
|
405
|
+
* lleva a ningún sitio.
|
|
406
|
+
*
|
|
407
|
+
* El contrato hacia fuera no cambia — mismos códigos, mismo `outputPayload`,
|
|
408
|
+
* mismos logs— salvo que ahora es asíncrono, que es lo que cuesta un proceso.
|
|
409
|
+
*/
|
|
410
|
+
async function executeCode(entity, payload, workspacePayloads) {
|
|
411
|
+
const start = Date.now();
|
|
412
|
+
const timeout = entity.timeout || 5000;
|
|
413
|
+
let output;
|
|
414
|
+
let logs;
|
|
415
|
+
try {
|
|
416
|
+
const r = await (0, code_runner_1.runTenantCode)(entity.code, { payload, headers: {}, meta: {}, workspacePayloads }, timeout);
|
|
417
|
+
output = r.output;
|
|
418
|
+
logs = r.logs;
|
|
419
|
+
}
|
|
420
|
+
catch (err) {
|
|
421
|
+
// Los logs que alcanzó a emitir antes de reventar son justo los que el
|
|
422
|
+
// usuario necesita para entender por qué. `CodeNodeError` los trae del
|
|
423
|
+
// worker; el hijo los cuelga del Error igual, así que este camino no
|
|
424
|
+
// necesita saber cuál de los dos corrió.
|
|
425
|
+
const conLogs = err;
|
|
426
|
+
return {
|
|
427
|
+
statusCode: 500,
|
|
428
|
+
responseBody: JSON.stringify({
|
|
429
|
+
error: conLogs?.message ?? String(err),
|
|
430
|
+
logs: conLogs?.logs ?? [],
|
|
431
|
+
}),
|
|
432
|
+
latencyMs: Date.now() - start,
|
|
433
|
+
logs: conLogs?.logs ?? [],
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
if (output == null) {
|
|
437
|
+
return {
|
|
438
|
+
statusCode: 204,
|
|
439
|
+
responseBody: JSON.stringify({ blocked: true, logs }),
|
|
440
|
+
latencyMs: Date.now() - start,
|
|
441
|
+
outputPayload: undefined,
|
|
442
|
+
logs,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
const outputPayload = typeof output === 'object' && !Array.isArray(output)
|
|
446
|
+
? output
|
|
447
|
+
: { result: output };
|
|
448
|
+
return {
|
|
449
|
+
statusCode: 200,
|
|
450
|
+
responseBody: JSON.stringify(outputPayload),
|
|
451
|
+
latencyMs: Date.now() - start,
|
|
452
|
+
outputPayload,
|
|
453
|
+
logs,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
/** If the executor didn't set outputPayload, try to parse it from responseBody. */
|
|
457
|
+
function ensureOutputPayload(result) {
|
|
458
|
+
if (result.outputPayload !== undefined)
|
|
459
|
+
return result;
|
|
460
|
+
// tryParseJson rather than tryParseJsonObject: an array body is a legitimate
|
|
461
|
+
// payload here, and the object-only helper would quietly drop it.
|
|
462
|
+
const parsed = (0, try_parse_1.tryParseJson)(result.responseBody);
|
|
463
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
464
|
+
return { ...result, outputPayload: parsed };
|
|
465
|
+
}
|
|
466
|
+
return result;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Loop fan-out: turn the iterable field into one payload per item, each
|
|
470
|
+
* stamped with its position. The loop node owns its own iteration and
|
|
471
|
+
* LoopState, which is why it has no registered handler — the caller drives
|
|
472
|
+
* the iterations from this list.
|
|
473
|
+
*/
|
|
474
|
+
function executeLoopFanOut(entity, payload) {
|
|
475
|
+
// Extract iterable array — use _meta.iterateField or entity.iterableField
|
|
476
|
+
const loopMeta = payload?._meta;
|
|
477
|
+
const loopField = loopMeta?.iterable && loopMeta.iterateField
|
|
478
|
+
? loopMeta.iterateField
|
|
479
|
+
: (entity.iterableField ?? '').trim() || '';
|
|
480
|
+
const loopItems = loopField ? payload?.[loopField] : payload;
|
|
481
|
+
common_1.Logger.debug(`[Loop] loopField="${loopField}", iterableField="${entity.iterableField}", payloadKeys=${Object.keys(payload)}, isArray=${Array.isArray(loopItems)}`, 'NodeExecutors');
|
|
482
|
+
if (!Array.isArray(loopItems)) {
|
|
483
|
+
return {
|
|
484
|
+
statusCode: 400,
|
|
485
|
+
responseBody: JSON.stringify({
|
|
486
|
+
error: `Field "${loopField || 'payload'}" is not an array`,
|
|
487
|
+
}),
|
|
488
|
+
latencyMs: 0,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
// Build per-iteration payloads
|
|
492
|
+
const perIteration = loopItems.map((item, i) => {
|
|
493
|
+
const base = typeof item === 'object' && item !== null && !Array.isArray(item)
|
|
494
|
+
? { ...item }
|
|
495
|
+
: { value: item };
|
|
496
|
+
return {
|
|
497
|
+
_meta: {
|
|
498
|
+
loopIndex: i,
|
|
499
|
+
loopTotal: loopItems.length,
|
|
500
|
+
isFirst: i === 0,
|
|
501
|
+
isLast: i === loopItems.length - 1,
|
|
502
|
+
},
|
|
503
|
+
...base,
|
|
504
|
+
};
|
|
505
|
+
});
|
|
506
|
+
const outputPayload = {
|
|
507
|
+
_meta: { iterable: true, iterateField: 'items', count: loopItems.length },
|
|
508
|
+
items: perIteration,
|
|
509
|
+
};
|
|
510
|
+
return {
|
|
511
|
+
statusCode: 200,
|
|
512
|
+
responseBody: JSON.stringify(outputPayload),
|
|
513
|
+
latencyMs: 0,
|
|
514
|
+
outputPayload,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* The seven node types that do NOT call `registerNodeHandler`, and what
|
|
519
|
+
* Run Pipeline does for each. Everything else executes through its
|
|
520
|
+
* registered handler — the same object prod runs — so the two cycles
|
|
521
|
+
* cannot drift.
|
|
522
|
+
*
|
|
523
|
+
* This list is deliberately explicit rather than a `default:` arm. A
|
|
524
|
+
* `default` silently absorbs any node type nobody wired up, which is
|
|
525
|
+
* exactly how the Limit node came to return 15 of 15 items in Run
|
|
526
|
+
* Pipeline while prod limited correctly. A missing entry here is a
|
|
527
|
+
* loud `undefined`, and `architecture.spec.ts` asserts the set matches
|
|
528
|
+
* the types that actually lack a handler.
|
|
529
|
+
*/
|
|
530
|
+
exports.HANDLERLESS_NODE_TYPES = {
|
|
531
|
+
/** Annotation — never executes. */
|
|
532
|
+
stickyNote: { run: 'passthrough', telemetrySource: 'sticky_note' },
|
|
533
|
+
// Sources. They execute by being *called* (an HTTP request arrives, a cron
|
|
534
|
+
// fires, a call connects), not by being dispatched to, so the payload they
|
|
535
|
+
// hand downstream is the payload they received.
|
|
536
|
+
webhook: { run: 'passthrough', telemetrySource: 'entry_webhook' },
|
|
537
|
+
scheduledWorkflow: {
|
|
538
|
+
run: 'passthrough',
|
|
539
|
+
telemetrySource: 'scheduled_webhook',
|
|
540
|
+
},
|
|
541
|
+
voiceAgent: { run: 'passthrough', telemetrySource: 'voice_agent' },
|
|
542
|
+
/** Queue-based: prod enqueues, Run Pipeline has nothing to wait on. */
|
|
543
|
+
delay: { run: 'passthrough', telemetrySource: 'delay_node' },
|
|
544
|
+
/** Waits on a human. Run Pipeline cannot block on a click. */
|
|
545
|
+
approval: { run: 'passthrough', telemetrySource: 'approval_node' },
|
|
546
|
+
/** pipeline-run.service collects the inputs itself and merges them. */
|
|
547
|
+
merge: { run: 'passthrough', telemetrySource: 'merge_node' },
|
|
548
|
+
/** Owns its own iteration + LoopState; pipeline-run drives it directly. */
|
|
549
|
+
loop: { run: 'custom', telemetrySource: 'loop_node' },
|
|
550
|
+
/** Counter lives in Redis; prod dispatches it from event-pipeline. */
|
|
551
|
+
rateLimiter: { run: 'custom', telemetrySource: 'rate_limiter_node' },
|
|
552
|
+
};
|
|
553
|
+
function isHandlerless(nodeType) {
|
|
554
|
+
return nodeType in exports.HANDLERLESS_NODE_TYPES;
|
|
555
|
+
}
|