@teleporthq/teleport-plugin-next-workflows 0.45.14 → 0.45.16
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/__tests__/cron-route-loop-templateparams.test.ts +5 -4
- package/__tests__/custom-js-loop-scope.test.ts +10 -5
- package/__tests__/loop-collection-unwrap.test.ts +6 -1
- package/__tests__/server-loop-body-branches.test.ts +188 -0
- package/__tests__/server-loop-types.test.ts +371 -0
- package/__tests__/server-nested-loops.test.ts +392 -0
- package/__tests__/streaming-route-if-branch.test.ts +107 -0
- package/dist/cjs/api-route-generator.d.ts.map +1 -1
- package/dist/cjs/api-route-generator.js +57 -248
- package/dist/cjs/api-route-generator.js.map +1 -1
- package/dist/cjs/executor-generator.d.ts.map +1 -1
- package/dist/cjs/executor-generator.js +84 -27
- package/dist/cjs/executor-generator.js.map +1 -1
- package/dist/cjs/server-runtime-code.d.ts.map +1 -1
- package/dist/cjs/server-runtime-code.js +67 -0
- package/dist/cjs/server-runtime-code.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/api-route-generator.d.ts.map +1 -1
- package/dist/esm/api-route-generator.js +57 -248
- package/dist/esm/api-route-generator.js.map +1 -1
- package/dist/esm/executor-generator.d.ts.map +1 -1
- package/dist/esm/executor-generator.js +84 -27
- package/dist/esm/executor-generator.js.map +1 -1
- package/dist/esm/server-runtime-code.d.ts.map +1 -1
- package/dist/esm/server-runtime-code.js +67 -0
- package/dist/esm/server-runtime-code.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/api-route-generator.ts +57 -248
- package/src/executor-generator.ts +84 -27
- package/src/server-runtime-code.ts +67 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import {
|
|
2
|
+
generateCronAPIRoute,
|
|
3
|
+
generateServerSegmentAPIRoute,
|
|
4
|
+
generateStreamingServerSegmentAPIRoute,
|
|
5
|
+
} from '../src/api-route-generator'
|
|
6
|
+
import type { WorkflowSegment } from '../src/types'
|
|
7
|
+
import { loadServerRuntime } from './_helpers/load-server-runtime'
|
|
8
|
+
import { withEnv } from './_helpers/fake-fetch'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A loop inside the body of another loop runs all of its passes on every pass
|
|
12
|
+
* of the outer one. The server routes ran an outer body as one flat list: the
|
|
13
|
+
* inner loop node fell through to a stub and its body ran once per outer pass.
|
|
14
|
+
* Each route kind is booted here and run with nested loops.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const OUTER_ID = 'outer-1'
|
|
18
|
+
const MIDDLE_ID = 'middle-1'
|
|
19
|
+
const INNER_ID = 'inner-1'
|
|
20
|
+
const MAKE_ID = 'make-1'
|
|
21
|
+
const IF_ID = 'if-1'
|
|
22
|
+
const BODY_ID = 'body-1'
|
|
23
|
+
const HIGH_ID = 'high-1'
|
|
24
|
+
const LOW_ID = 'low-1'
|
|
25
|
+
const AFTER_ID = 'after-1'
|
|
26
|
+
const REPORT_ID = 'report-1'
|
|
27
|
+
const INNER_ITER_ID = INNER_ID + '_iter'
|
|
28
|
+
const RUNS_KEY = '__serverNestedLoopRuns'
|
|
29
|
+
const MADE_KEY = '__serverNestedLoopMade'
|
|
30
|
+
|
|
31
|
+
type GraphNode = Record<string, unknown> & { id: string }
|
|
32
|
+
type GraphEdge = Record<string, unknown> & { id: string; source: string; target: string }
|
|
33
|
+
interface Graph {
|
|
34
|
+
nodes: GraphNode[]
|
|
35
|
+
edges: GraphEdge[]
|
|
36
|
+
}
|
|
37
|
+
interface Run {
|
|
38
|
+
status: number
|
|
39
|
+
loopResult: (loopId: string) => unknown
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const runs = (): unknown[] => (globalThis as Record<string, any>)[RUNS_KEY] || []
|
|
43
|
+
|
|
44
|
+
const loopNode = (id: string, stepNumber: number, config: Record<string, unknown>): GraphNode => ({
|
|
45
|
+
id,
|
|
46
|
+
type: 'general-loop',
|
|
47
|
+
config,
|
|
48
|
+
stepNumber,
|
|
49
|
+
label: id,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const recordingNode = (id: string, stepNumber: number): GraphNode => ({
|
|
53
|
+
id,
|
|
54
|
+
type: 'general-custom-js',
|
|
55
|
+
config: {
|
|
56
|
+
code: [
|
|
57
|
+
'function customHandler() {',
|
|
58
|
+
` globalThis.${RUNS_KEY} = (globalThis.${RUNS_KEY} || []).concat('${id}');`,
|
|
59
|
+
` return { n: globalThis.${RUNS_KEY}.length };`,
|
|
60
|
+
'}',
|
|
61
|
+
].join('\n'),
|
|
62
|
+
context: 'server',
|
|
63
|
+
},
|
|
64
|
+
stepNumber,
|
|
65
|
+
label: id,
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const bodyEntry = (id: string, loopId: string, target: string): GraphEdge => ({
|
|
69
|
+
id,
|
|
70
|
+
source: loopId,
|
|
71
|
+
target,
|
|
72
|
+
sourceHandle: 'loop',
|
|
73
|
+
targetHandle: 'loop-body-in',
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const loopBack = (id: string, source: string, loopId: string): GraphEdge => ({
|
|
77
|
+
id,
|
|
78
|
+
source,
|
|
79
|
+
target: loopId,
|
|
80
|
+
sourceHandle: 'loop-body-out',
|
|
81
|
+
targetHandle: 'loop-back',
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const exitTo = (id: string, loopId: string, target: string): GraphEdge => ({
|
|
85
|
+
id,
|
|
86
|
+
source: loopId,
|
|
87
|
+
target,
|
|
88
|
+
sourceHandle: 'exit',
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const forLoopInForLoop = (): Graph => ({
|
|
92
|
+
nodes: [
|
|
93
|
+
loopNode(OUTER_ID, 1, { loopType: 'for', endIndex: 2 }),
|
|
94
|
+
loopNode(INNER_ID, 2, { loopType: 'for', endIndex: 3 }),
|
|
95
|
+
recordingNode(BODY_ID, 3),
|
|
96
|
+
recordingNode(AFTER_ID, 4),
|
|
97
|
+
],
|
|
98
|
+
edges: [
|
|
99
|
+
bodyEntry('e1', OUTER_ID, INNER_ID),
|
|
100
|
+
bodyEntry('e2', INNER_ID, BODY_ID),
|
|
101
|
+
loopBack('e3', BODY_ID, INNER_ID),
|
|
102
|
+
loopBack('e4', INNER_ID, OUTER_ID),
|
|
103
|
+
exitTo('e5', OUTER_ID, AFTER_ID),
|
|
104
|
+
],
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const threeLoopsDeep = (): Graph => ({
|
|
108
|
+
nodes: [
|
|
109
|
+
loopNode(OUTER_ID, 1, { loopType: 'for', endIndex: 2 }),
|
|
110
|
+
loopNode(MIDDLE_ID, 2, { loopType: 'forEach', collection: ['a', 'b'] }),
|
|
111
|
+
loopNode(INNER_ID, 3, { loopType: 'map', collection: ['x', 'y', 'z'] }),
|
|
112
|
+
recordingNode(BODY_ID, 4),
|
|
113
|
+
recordingNode(AFTER_ID, 5),
|
|
114
|
+
],
|
|
115
|
+
edges: [
|
|
116
|
+
bodyEntry('e1', OUTER_ID, MIDDLE_ID),
|
|
117
|
+
bodyEntry('e2', MIDDLE_ID, INNER_ID),
|
|
118
|
+
bodyEntry('e3', INNER_ID, BODY_ID),
|
|
119
|
+
loopBack('e4', BODY_ID, INNER_ID),
|
|
120
|
+
loopBack('e5', INNER_ID, MIDDLE_ID),
|
|
121
|
+
loopBack('e6', MIDDLE_ID, OUTER_ID),
|
|
122
|
+
exitTo('e7', OUTER_ID, AFTER_ID),
|
|
123
|
+
],
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The outer body first makes a collection (one item on the first outer pass,
|
|
128
|
+
* two on the second), then walks it with the inner loop, whose body records
|
|
129
|
+
* the item it was handed.
|
|
130
|
+
*/
|
|
131
|
+
const forEachOverWhatTheOuterPassMade = (): Graph => ({
|
|
132
|
+
nodes: [
|
|
133
|
+
loopNode(OUTER_ID, 1, { loopType: 'for', endIndex: 2 }),
|
|
134
|
+
{
|
|
135
|
+
id: MAKE_ID,
|
|
136
|
+
type: 'general-custom-js',
|
|
137
|
+
config: {
|
|
138
|
+
code: [
|
|
139
|
+
'function customHandler() {',
|
|
140
|
+
` globalThis.${MADE_KEY} = (globalThis.${MADE_KEY} || 0) + 1;`,
|
|
141
|
+
` return { items: globalThis.${MADE_KEY} === 1 ? ['a'] : ['b', 'c'] };`,
|
|
142
|
+
'}',
|
|
143
|
+
].join('\n'),
|
|
144
|
+
context: 'server',
|
|
145
|
+
},
|
|
146
|
+
stepNumber: 2,
|
|
147
|
+
label: 'Make The Items',
|
|
148
|
+
},
|
|
149
|
+
loopNode(INNER_ID, 3, {
|
|
150
|
+
loopType: 'forEach',
|
|
151
|
+
collection: { type: 'workflowContext', nodeId: MAKE_ID, path: [MAKE_ID, 'items'] },
|
|
152
|
+
}),
|
|
153
|
+
{
|
|
154
|
+
id: BODY_ID,
|
|
155
|
+
type: 'general-custom-js',
|
|
156
|
+
config: {
|
|
157
|
+
code: [
|
|
158
|
+
'function customHandler(params) {',
|
|
159
|
+
` globalThis.${RUNS_KEY} = (globalThis.${RUNS_KEY} || []).concat(params.item);`,
|
|
160
|
+
' return { ok: true };',
|
|
161
|
+
'}',
|
|
162
|
+
].join('\n'),
|
|
163
|
+
params: {
|
|
164
|
+
item: { type: 'workflowContext', nodeId: INNER_ID, path: [INNER_ID, 'currentItem'] },
|
|
165
|
+
},
|
|
166
|
+
context: 'server',
|
|
167
|
+
},
|
|
168
|
+
stepNumber: 4,
|
|
169
|
+
label: 'Record The Item',
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
edges: [
|
|
173
|
+
bodyEntry('e1', OUTER_ID, MAKE_ID),
|
|
174
|
+
{ id: 'e2', source: MAKE_ID, target: INNER_ID },
|
|
175
|
+
bodyEntry('e3', INNER_ID, BODY_ID),
|
|
176
|
+
loopBack('e4', BODY_ID, INNER_ID),
|
|
177
|
+
loopBack('e5', INNER_ID, OUTER_ID),
|
|
178
|
+
],
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
const branchInTheInnerBody = (): Graph => ({
|
|
182
|
+
nodes: [
|
|
183
|
+
loopNode(OUTER_ID, 1, { loopType: 'for', endIndex: 2 }),
|
|
184
|
+
loopNode(INNER_ID, 2, { loopType: 'for', endIndex: 4 }),
|
|
185
|
+
{
|
|
186
|
+
id: IF_ID,
|
|
187
|
+
type: 'general-if-statement',
|
|
188
|
+
config: {
|
|
189
|
+
conditionType: 'simple-comparison',
|
|
190
|
+
leftValue: {
|
|
191
|
+
type: 'workflowContext',
|
|
192
|
+
nodeId: INNER_ITER_ID,
|
|
193
|
+
path: [INNER_ITER_ID, 'index'],
|
|
194
|
+
},
|
|
195
|
+
operator: '>=',
|
|
196
|
+
rightValue: 2,
|
|
197
|
+
},
|
|
198
|
+
stepNumber: 3,
|
|
199
|
+
label: 'Index Is High',
|
|
200
|
+
},
|
|
201
|
+
recordingNode(HIGH_ID, 4),
|
|
202
|
+
recordingNode(LOW_ID, 5),
|
|
203
|
+
recordingNode(AFTER_ID, 6),
|
|
204
|
+
],
|
|
205
|
+
edges: [
|
|
206
|
+
bodyEntry('e1', OUTER_ID, INNER_ID),
|
|
207
|
+
bodyEntry('e2', INNER_ID, IF_ID),
|
|
208
|
+
{ id: 'e3', source: IF_ID, target: HIGH_ID, sourceHandle: 'true' },
|
|
209
|
+
{ id: 'e4', source: IF_ID, target: LOW_ID, sourceHandle: 'false' },
|
|
210
|
+
loopBack('e5', HIGH_ID, INNER_ID),
|
|
211
|
+
loopBack('e6', LOW_ID, INNER_ID),
|
|
212
|
+
loopBack('e7', INNER_ID, OUTER_ID),
|
|
213
|
+
exitTo('e8', OUTER_ID, AFTER_ID),
|
|
214
|
+
],
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
const requireRuntime = (extra: Record<string, unknown> = {}) => {
|
|
218
|
+
const runtime = loadServerRuntime()
|
|
219
|
+
return (id: string): unknown => {
|
|
220
|
+
if (id.endsWith('/utils/workflows/server-runtime')) {
|
|
221
|
+
return runtime
|
|
222
|
+
}
|
|
223
|
+
const known = Object.keys(extra).find((suffix) => id.endsWith(suffix))
|
|
224
|
+
if (known) {
|
|
225
|
+
return extra[known]
|
|
226
|
+
}
|
|
227
|
+
throw new Error('Unexpected require: ' + id)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const bootRoute = (source: string, requireFn: (id: string) => unknown) => {
|
|
232
|
+
const routeModule: { exports: any } = { exports: {} }
|
|
233
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
234
|
+
new Function('module', 'exports', 'require', source)(routeModule, routeModule.exports, requireFn)
|
|
235
|
+
return routeModule.exports as (req: unknown, res: unknown) => Promise<void>
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const jsonReply = () => {
|
|
239
|
+
const reply = { status: 0, body: undefined as any }
|
|
240
|
+
const res = {
|
|
241
|
+
status(code: number) {
|
|
242
|
+
reply.status = code
|
|
243
|
+
return this
|
|
244
|
+
},
|
|
245
|
+
json(payload: unknown) {
|
|
246
|
+
reply.body = payload
|
|
247
|
+
return this
|
|
248
|
+
},
|
|
249
|
+
setHeader: (): void => undefined,
|
|
250
|
+
}
|
|
251
|
+
return { reply, res }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const runSegmentRoute =
|
|
255
|
+
(generate: (segment: WorkflowSegment) => string) =>
|
|
256
|
+
async (graph: Graph): Promise<Run> => {
|
|
257
|
+
const segment = {
|
|
258
|
+
id: 'seg-nested-loops',
|
|
259
|
+
env: 'server',
|
|
260
|
+
nodeIds: graph.nodes.map((node) => node.id),
|
|
261
|
+
nodes: graph.nodes,
|
|
262
|
+
edges: graph.edges,
|
|
263
|
+
} as unknown as WorkflowSegment
|
|
264
|
+
const handler = bootRoute(generate(segment), requireRuntime())
|
|
265
|
+
const { reply, res } = jsonReply()
|
|
266
|
+
await handler({ method: 'POST', body: { context: {} }, headers: { host: 'site.example' } }, res)
|
|
267
|
+
return { status: reply.status, loopResult: (loopId) => reply.body.results[loopId] }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* A scheduled route replies with no results, so its last step is a custom node
|
|
272
|
+
* that is handed the result of every loop of the graph.
|
|
273
|
+
*/
|
|
274
|
+
const runScheduledRoute = async (graph: Graph): Promise<Run> => {
|
|
275
|
+
const loopIds = graph.nodes.filter((node) => node.type === 'general-loop').map((node) => node.id)
|
|
276
|
+
const parameters: Record<string, unknown> = {}
|
|
277
|
+
loopIds.forEach((loopId) => {
|
|
278
|
+
parameters[loopId] = { type: 'workflowContext', nodeId: loopId, path: [loopId] }
|
|
279
|
+
})
|
|
280
|
+
const reportNode = {
|
|
281
|
+
id: REPORT_ID,
|
|
282
|
+
type: 'general-custom-node',
|
|
283
|
+
config: { customNodeId: 'cn-report', parameters },
|
|
284
|
+
stepNumber: graph.nodes.length + 1,
|
|
285
|
+
label: 'Report The Loops',
|
|
286
|
+
}
|
|
287
|
+
const workflow: any = {
|
|
288
|
+
id: 'wf-scheduled-nested-loops',
|
|
289
|
+
name: 'Scheduled Nested Loops',
|
|
290
|
+
trigger: {
|
|
291
|
+
type: 'event-cron-triggered',
|
|
292
|
+
nodeId: 't',
|
|
293
|
+
scope: 'global',
|
|
294
|
+
config: { schedule: '5 * * * *', urlPath: 'cron_nested_loops' },
|
|
295
|
+
},
|
|
296
|
+
nodes: [...graph.nodes, reportNode],
|
|
297
|
+
edges: [
|
|
298
|
+
{ id: 'e0', source: 't', target: OUTER_ID },
|
|
299
|
+
...graph.edges,
|
|
300
|
+
exitTo('e-report', OUTER_ID, REPORT_ID),
|
|
301
|
+
],
|
|
302
|
+
}
|
|
303
|
+
const customNodes: Record<string, unknown> = {
|
|
304
|
+
'cn-report': { id: 'cn-report', name: 'Report', nodes: [], edges: [] },
|
|
305
|
+
}
|
|
306
|
+
let reported: Record<string, unknown> = {}
|
|
307
|
+
const customNodeModule = {
|
|
308
|
+
'cn-report': async (_context: unknown, handed: Record<string, unknown>) => {
|
|
309
|
+
reported = handed
|
|
310
|
+
return { ok: true }
|
|
311
|
+
},
|
|
312
|
+
}
|
|
313
|
+
const handler = bootRoute(
|
|
314
|
+
generateCronAPIRoute(workflow, customNodes as never),
|
|
315
|
+
requireRuntime({ '/utils/workflows/custom-nodes': customNodeModule })
|
|
316
|
+
)
|
|
317
|
+
const { reply, res } = jsonReply()
|
|
318
|
+
const req = {
|
|
319
|
+
method: 'POST',
|
|
320
|
+
url: '/api/workflows/cron_nested_loops',
|
|
321
|
+
headers: { host: 'site.example', 'x-forwarded-proto': 'https' },
|
|
322
|
+
body: {},
|
|
323
|
+
}
|
|
324
|
+
await withEnv({ TELEPORT_CRON_ALLOW_UNSIGNED: 'true', TELEPORT_CRON_SECRET: undefined }, () =>
|
|
325
|
+
handler(req, res)
|
|
326
|
+
)
|
|
327
|
+
return { status: reply.status, loopResult: (loopId) => reported[loopId] }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const clearRecordings = () => {
|
|
331
|
+
delete (globalThis as Record<string, any>)[RUNS_KEY]
|
|
332
|
+
delete (globalThis as Record<string, any>)[MADE_KEY]
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
beforeEach(clearRecordings)
|
|
336
|
+
afterAll(clearRecordings)
|
|
337
|
+
|
|
338
|
+
describe.each<[string, (graph: Graph) => Promise<Run>]>([
|
|
339
|
+
['a server segment route', runSegmentRoute(generateServerSegmentAPIRoute)],
|
|
340
|
+
['a streaming server segment route', runSegmentRoute(generateStreamingServerSegmentAPIRoute)],
|
|
341
|
+
['a scheduled route', runScheduledRoute],
|
|
342
|
+
])('%s runs a loop inside a loop', (_route, run) => {
|
|
343
|
+
it('runs every pass of the inner loop on every pass of the outer loop', async () => {
|
|
344
|
+
const result = await run(forLoopInForLoop())
|
|
345
|
+
|
|
346
|
+
expect(result.status).toBe(200)
|
|
347
|
+
expect(runs()).toEqual([BODY_ID, BODY_ID, BODY_ID, BODY_ID, BODY_ID, BODY_ID, AFTER_ID])
|
|
348
|
+
expect(result.loopResult(OUTER_ID)).toEqual({ completed: true, iterations: 2, results: [] })
|
|
349
|
+
expect(result.loopResult(INNER_ID)).toEqual({ completed: true, iterations: 3, results: [] })
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it('runs a loop in a loop in a loop', async () => {
|
|
353
|
+
const result = await run(threeLoopsDeep())
|
|
354
|
+
|
|
355
|
+
expect(result.status).toBe(200)
|
|
356
|
+
expect(runs()).toEqual([...Array(12).fill(BODY_ID), AFTER_ID])
|
|
357
|
+
expect(result.loopResult(OUTER_ID)).toEqual({ completed: true, iterations: 2, results: [] })
|
|
358
|
+
expect(result.loopResult(MIDDLE_ID)).toEqual({ completed: true, iterations: 2, results: [] })
|
|
359
|
+
expect(result.loopResult(INNER_ID)).toEqual({
|
|
360
|
+
completed: true,
|
|
361
|
+
iterations: 3,
|
|
362
|
+
results: [{ n: 10 }, { n: 11 }, { n: 12 }],
|
|
363
|
+
})
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
it('walks the collection the current outer pass made', async () => {
|
|
367
|
+
const result = await run(forEachOverWhatTheOuterPassMade())
|
|
368
|
+
|
|
369
|
+
expect(result.status).toBe(200)
|
|
370
|
+
expect(runs()).toEqual(['a', 'b', 'c'])
|
|
371
|
+
expect(result.loopResult(OUTER_ID)).toEqual({ completed: true, iterations: 2, results: [] })
|
|
372
|
+
expect(result.loopResult(INNER_ID)).toEqual({ completed: true, iterations: 2, results: [] })
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
it('takes one branch of the inner body per inner pass', async () => {
|
|
376
|
+
const result = await run(branchInTheInnerBody())
|
|
377
|
+
|
|
378
|
+
expect(result.status).toBe(200)
|
|
379
|
+
expect(runs()).toEqual([
|
|
380
|
+
LOW_ID,
|
|
381
|
+
LOW_ID,
|
|
382
|
+
HIGH_ID,
|
|
383
|
+
HIGH_ID,
|
|
384
|
+
LOW_ID,
|
|
385
|
+
LOW_ID,
|
|
386
|
+
HIGH_ID,
|
|
387
|
+
HIGH_ID,
|
|
388
|
+
AFTER_ID,
|
|
389
|
+
])
|
|
390
|
+
expect(result.loopResult(INNER_ID)).toEqual({ completed: true, iterations: 4, results: [] })
|
|
391
|
+
})
|
|
392
|
+
})
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { generateStreamingServerSegmentAPIRoute } from '../src/api-route-generator'
|
|
2
|
+
import type { WorkflowSegment } from '../src/types'
|
|
3
|
+
import { loadServerRuntime } from './_helpers/load-server-runtime'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A streaming segment route evaluated an if-statement and then ran both of its
|
|
7
|
+
* branches: it never recorded the branch not taken, and never read the record
|
|
8
|
+
* a switch made of its unmatched cases.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const RUNS_KEY = '__streamingIfBranchRuns'
|
|
12
|
+
|
|
13
|
+
const runs = (): string[] => (globalThis as Record<string, any>)[RUNS_KEY] || []
|
|
14
|
+
|
|
15
|
+
const recordingNode = (id: string, stepNumber: number) => ({
|
|
16
|
+
id,
|
|
17
|
+
type: 'general-custom-js',
|
|
18
|
+
config: {
|
|
19
|
+
code: [
|
|
20
|
+
'function customHandler() {',
|
|
21
|
+
` globalThis.${RUNS_KEY} = (globalThis.${RUNS_KEY} || []).concat('${id}');`,
|
|
22
|
+
' return { ok: true };',
|
|
23
|
+
'}',
|
|
24
|
+
].join('\n'),
|
|
25
|
+
context: 'server',
|
|
26
|
+
},
|
|
27
|
+
stepNumber,
|
|
28
|
+
label: id,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const segmentWhere = (leftValue: number): WorkflowSegment =>
|
|
32
|
+
({
|
|
33
|
+
id: 'seg-if',
|
|
34
|
+
env: 'server',
|
|
35
|
+
nodeIds: ['if-1', 'yes-1', 'no-1'],
|
|
36
|
+
nodes: [
|
|
37
|
+
{
|
|
38
|
+
id: 'if-1',
|
|
39
|
+
type: 'general-if-statement',
|
|
40
|
+
config: { conditionType: 'simple-comparison', leftValue, operator: '===', rightValue: 1 },
|
|
41
|
+
stepNumber: 1,
|
|
42
|
+
label: 'Is One',
|
|
43
|
+
},
|
|
44
|
+
recordingNode('yes-1', 2),
|
|
45
|
+
recordingNode('no-1', 3),
|
|
46
|
+
],
|
|
47
|
+
edges: [
|
|
48
|
+
{ id: 'e1', source: 'if-1', target: 'yes-1', sourceHandle: 'true' },
|
|
49
|
+
{ id: 'e2', source: 'if-1', target: 'no-1', sourceHandle: 'false' },
|
|
50
|
+
],
|
|
51
|
+
} as unknown as WorkflowSegment)
|
|
52
|
+
|
|
53
|
+
const run = async (segment: WorkflowSegment) => {
|
|
54
|
+
const runtime = loadServerRuntime()
|
|
55
|
+
const routeModule: { exports: any } = { exports: {} }
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
57
|
+
new Function('module', 'exports', 'require', generateStreamingServerSegmentAPIRoute(segment))(
|
|
58
|
+
routeModule,
|
|
59
|
+
routeModule.exports,
|
|
60
|
+
(id: string): unknown => {
|
|
61
|
+
if (id.endsWith('/utils/workflows/server-runtime')) {
|
|
62
|
+
return runtime
|
|
63
|
+
}
|
|
64
|
+
throw new Error('Unexpected require: ' + id)
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
const reply = { status: 0 }
|
|
68
|
+
const res = {
|
|
69
|
+
status(code: number) {
|
|
70
|
+
reply.status = code
|
|
71
|
+
return this
|
|
72
|
+
},
|
|
73
|
+
json() {
|
|
74
|
+
return this
|
|
75
|
+
},
|
|
76
|
+
setHeader: (): void => undefined,
|
|
77
|
+
}
|
|
78
|
+
await routeModule.exports(
|
|
79
|
+
{ method: 'POST', body: { context: {} }, headers: { host: 'site.example' } },
|
|
80
|
+
res
|
|
81
|
+
)
|
|
82
|
+
return reply
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
beforeEach(() => {
|
|
86
|
+
delete (globalThis as Record<string, any>)[RUNS_KEY]
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
afterAll(() => {
|
|
90
|
+
delete (globalThis as Record<string, any>)[RUNS_KEY]
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('a streaming server segment route runs one branch of an if-statement', () => {
|
|
94
|
+
it('runs only the true branch when the condition holds', async () => {
|
|
95
|
+
const reply = await run(segmentWhere(1))
|
|
96
|
+
|
|
97
|
+
expect(reply.status).toBe(200)
|
|
98
|
+
expect(runs()).toEqual(['yes-1'])
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('runs only the false branch when it does not', async () => {
|
|
102
|
+
const reply = await run(segmentWhere(2))
|
|
103
|
+
|
|
104
|
+
expect(reply.status).toBe(200)
|
|
105
|
+
expect(runs()).toEqual(['no-1'])
|
|
106
|
+
})
|
|
107
|
+
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-route-generator.d.ts","sourceRoot":"","sources":["../../src/api-route-generator.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,aAAa,EAGb,iBAAiB,EACjB,sBAAsB,EACtB,sBAAsB,EACvB,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAE9C,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAA;AAuEvE,MAAM,WAAW,yBAAyB;IACxC,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;;OAIG;IACH,KAAK,CAAC,EAAE,YAAY,CAAA;IACpB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAA;CACrD;AAED,OAAO,EAAE,qBAAqB,EAAE,CAAA;AAEhC,wEAAwE;AACxE,eAAO,MAAM,wBAAwB;;;CAAiC,CAAA;AAEtE,iFAAiF;AACjF,eAAO,MAAM,oBAAoB,4CAA4C,CAAA;AA+O7E,eAAO,MAAM,6BAA6B,YAC/B,eAAe,iBACT,MAAM,eACR,sBAAsB,YAC1B,yBAAyB,KACjC,
|
|
1
|
+
{"version":3,"file":"api-route-generator.d.ts","sourceRoot":"","sources":["../../src/api-route-generator.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,aAAa,EAGb,iBAAiB,EACjB,sBAAsB,EACtB,sBAAsB,EACvB,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAE9C,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAA;AAuEvE,MAAM,WAAW,yBAAyB;IACxC,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;;OAIG;IACH,KAAK,CAAC,EAAE,YAAY,CAAA;IACpB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAA;CACrD;AAED,OAAO,EAAE,qBAAqB,EAAE,CAAA;AAEhC,wEAAwE;AACxE,eAAO,MAAM,wBAAwB;;;CAAiC,CAAA;AAEtE,iFAAiF;AACjF,eAAO,MAAM,oBAAoB,4CAA4C,CAAA;AA+O7E,eAAO,MAAM,6BAA6B,YAC/B,eAAe,iBACT,MAAM,eACR,sBAAsB,YAC1B,yBAAyB,KACjC,MAuSF,CAAA;AAED,eAAO,MAAM,kBAAkB,YAAa,eAAe,KAAG,OAE7D,CAAA;AAED,eAAO,MAAM,sCAAsC,YACxC,eAAe,iBACT,MAAM,eACR,sBAAsB,YAC1B,yBAAyB,KACjC,MA+ZF,CAAA;AA6ID;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,aA4D5B,CAAA;AAEF,qDAAqD;AACrD,MAAM,WAAW,0BAA0B;IACzC,kEAAkE;IAClE,eAAe,CAAC,EAAE,OAAO,CAAA;CAC1B;AAqBD,eAAO,MAAM,oBAAoB,aACrB,YAAY,gBACR,OAAO,MAAM,EAAE,sBAAsB,CAAC,YAC3C,0BAA0B,KAClC,MA0HF,CAAA;AAED,eAAO,MAAM,mBAAmB,eAClB,MAAM,aACP,MAAM,iBACF,MAAM,KACpB,MA4BF,CAAA;AAED,eAAO,MAAM,oBAAoB,aAAc,YAAY,KAAG,MAa7D,CAAA;AAmPD,uHAAuH;AACvH,eAAO,MAAM,uBAAuB,kBAAmB,iBAAiB,KAAG,OAES,CAAA;AAEpF,eAAO,MAAM,+BAA+B,aAChC,YAAY,gBACR,OAAO,MAAM,EAAE,sBAAsB,CAAC,YAC3C,0BAA0B,KAClC,MAkOF,CAAA;AAkED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,uBAAuB,aACxB,YAAY,gBACR,OAAO,MAAM,EAAE,sBAAsB,CAAC,KACnD,MA8KF,CAAA;AAED,eAAO,MAAM,uBAAuB,aAAc,YAAY,KAAG,MAiBhE,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,0BAA0B,cAC1B,aAAa,GAAG,SAAS,cACxB,MAAM,KACjB,MAAM,GAAG,IAYX,CAAA;AAED,eAAO,MAAM,mBAAmB,aAAc,YAAY,KAAG,MAAM,EAqBlE,CAAA"}
|