@teleporthq/teleport-plugin-next-workflows 0.43.35 → 0.43.37

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.
Files changed (50) hide show
  1. package/__tests__/ai-error-contract.test.ts +149 -0
  2. package/__tests__/data-api-safe-query.test.ts +16 -16
  3. package/__tests__/node-handler-file-inline-map.test.ts +114 -0
  4. package/__tests__/raw-query-param-binding.test.ts +18 -17
  5. package/__tests__/streaming-on-end-env-filter.test.ts +405 -0
  6. package/dist/cjs/api-route-generator.d.ts.map +1 -1
  7. package/dist/cjs/api-route-generator.js +14 -8
  8. package/dist/cjs/api-route-generator.js.map +1 -1
  9. package/dist/cjs/data-api-route-generator.d.ts.map +1 -1
  10. package/dist/cjs/data-api-route-generator.js +13 -2
  11. package/dist/cjs/data-api-route-generator.js.map +1 -1
  12. package/dist/cjs/executor-generator.d.ts.map +1 -1
  13. package/dist/cjs/executor-generator.js +47 -5
  14. package/dist/cjs/executor-generator.js.map +1 -1
  15. package/dist/cjs/nodes/general/general-custom-js.d.ts.map +1 -1
  16. package/dist/cjs/nodes/general/general-custom-js.js +7 -0
  17. package/dist/cjs/nodes/general/general-custom-js.js.map +1 -1
  18. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  19. package/dist/cjs/workflow-component-plugin.js +9 -3
  20. package/dist/cjs/workflow-component-plugin.js.map +1 -1
  21. package/dist/cjs/workflow-project-plugin.d.ts +1 -0
  22. package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
  23. package/dist/cjs/workflow-project-plugin.js +83 -5
  24. package/dist/cjs/workflow-project-plugin.js.map +1 -1
  25. package/dist/esm/api-route-generator.d.ts.map +1 -1
  26. package/dist/esm/api-route-generator.js +14 -8
  27. package/dist/esm/api-route-generator.js.map +1 -1
  28. package/dist/esm/data-api-route-generator.d.ts.map +1 -1
  29. package/dist/esm/data-api-route-generator.js +13 -2
  30. package/dist/esm/data-api-route-generator.js.map +1 -1
  31. package/dist/esm/executor-generator.d.ts.map +1 -1
  32. package/dist/esm/executor-generator.js +47 -5
  33. package/dist/esm/executor-generator.js.map +1 -1
  34. package/dist/esm/nodes/general/general-custom-js.d.ts.map +1 -1
  35. package/dist/esm/nodes/general/general-custom-js.js +7 -0
  36. package/dist/esm/nodes/general/general-custom-js.js.map +1 -1
  37. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  38. package/dist/esm/workflow-component-plugin.js +9 -3
  39. package/dist/esm/workflow-component-plugin.js.map +1 -1
  40. package/dist/esm/workflow-project-plugin.d.ts +1 -0
  41. package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
  42. package/dist/esm/workflow-project-plugin.js +83 -5
  43. package/dist/esm/workflow-project-plugin.js.map +1 -1
  44. package/package.json +2 -2
  45. package/src/api-route-generator.ts +14 -8
  46. package/src/data-api-route-generator.ts +13 -2
  47. package/src/executor-generator.ts +47 -5
  48. package/src/nodes/general/general-custom-js.ts +8 -0
  49. package/src/workflow-component-plugin.ts +15 -10
  50. package/src/workflow-project-plugin.ts +87 -4
@@ -0,0 +1,149 @@
1
+ // Regression guard for "AI provider/auth failures treated as success".
2
+ //
3
+ // Every AI node handler (src/nodes/ai/*) signals failure with
4
+ // `{ error: true, message, code }` — a BOOLEAN error flag — while the executor
5
+ // error gates only tested `typeof result.error === 'string'`. A missing
6
+ // OPENAI key therefore sailed through as a successful node result and the
7
+ // pipeline limped into a downstream NOT NULL 500 (e.g. persisting a NULL chat
8
+ // answer). The gates now treat `result.error === true` as fatal too, via the
9
+ // shared isFatalNodeResult / fatalNodeResultMessage runtime helpers.
10
+
11
+ import { generateSharedRuntimeUtilsCode } from '../src/executor-generator'
12
+ import {
13
+ generateServerSegmentAPIRoute,
14
+ generateStreamingServerSegmentAPIRoute,
15
+ } from '../src/api-route-generator'
16
+ import type { WorkflowSegment } from '../src/types'
17
+
18
+ type UtilsModule = {
19
+ isFatalNodeResult: (result: unknown) => boolean
20
+ fatalNodeResultMessage: (result: Record<string, unknown>) => string
21
+ executeNodes: (
22
+ nodes: unknown[],
23
+ edges: unknown[],
24
+ context: Record<string, unknown>,
25
+ nodeHandlers: Record<string, unknown>,
26
+ workflowConfig: unknown,
27
+ callServerSegment: unknown,
28
+ executionId: string
29
+ ) => Promise<void>
30
+ }
31
+
32
+ function loadUtils(): UtilsModule {
33
+ const utilsModule = { exports: {} as unknown as UtilsModule }
34
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
35
+ new Function('module', 'exports', 'require', generateSharedRuntimeUtilsCode())(
36
+ utilsModule,
37
+ utilsModule.exports,
38
+ () => ({})
39
+ )
40
+ return utilsModule.exports
41
+ }
42
+
43
+ describe('isFatalNodeResult — error contract normalization', () => {
44
+ const utils = loadUtils()
45
+
46
+ it('treats the AI-node contract { error: true, message, code } as fatal', () => {
47
+ expect(
48
+ utils.isFatalNodeResult({ error: true, message: 'No API key', code: 'authentication_error' })
49
+ ).toBeTruthy()
50
+ })
51
+
52
+ it('keeps the legacy string-error contract fatal', () => {
53
+ expect(utils.isFatalNodeResult({ error: 'boom' })).toBeTruthy()
54
+ expect(utils.isFatalNodeResult({ success: false })).toBeTruthy()
55
+ })
56
+
57
+ it('does not flag successful results', () => {
58
+ expect(utils.isFatalNodeResult({ result: 'ok' })).toBeFalsy()
59
+ expect(utils.isFatalNodeResult({ error: false })).toBeFalsy()
60
+ expect(utils.isFatalNodeResult({ error: '' })).toBeFalsy()
61
+ expect(utils.isFatalNodeResult(null)).toBeFalsy()
62
+ expect(utils.isFatalNodeResult(undefined)).toBeFalsy()
63
+ })
64
+
65
+ it('surfaces the AI-node message when error is boolean', () => {
66
+ expect(
67
+ utils.fatalNodeResultMessage({ error: true, message: 'Incorrect API key provided' })
68
+ ).toBe('Incorrect API key provided')
69
+ expect(utils.fatalNodeResultMessage({ error: 'raw string error' })).toBe('raw string error')
70
+ expect(utils.fatalNodeResultMessage({ success: false })).toBe('Node execution failed')
71
+ })
72
+ })
73
+
74
+ describe('executeNodes halts on { error: true } and never reaches downstream nodes', () => {
75
+ const utils = loadUtils()
76
+
77
+ const nodes = [
78
+ { id: 'ai-1', type: 'ai-custom-prompt', config: {}, stepNumber: 1 },
79
+ { id: 'persist-1', type: 'data-create-item', config: {}, stepNumber: 2 },
80
+ ]
81
+ const edges = [{ id: 'e1', source: 'ai-1', target: 'persist-1' }]
82
+
83
+ it('throws with the provider message and skips the downstream persist node', async () => {
84
+ const persistCalls: unknown[] = []
85
+ const handlers = {
86
+ 'ai-custom-prompt': async () => ({
87
+ error: true,
88
+ message: 'AI provider authentication failed',
89
+ code: 'authentication_error',
90
+ }),
91
+ 'data-create-item': async () => {
92
+ persistCalls.push('persist')
93
+ return { success: true }
94
+ },
95
+ }
96
+
97
+ await expect(
98
+ utils.executeNodes(nodes, edges, {}, handlers, { nodes, edges }, null, 'exec-1')
99
+ ).rejects.toThrow('AI provider authentication failed')
100
+ expect(persistCalls).toHaveLength(0)
101
+ })
102
+
103
+ it('still runs downstream nodes for successful AI results', async () => {
104
+ const persistCalls: unknown[] = []
105
+ const handlers = {
106
+ 'ai-custom-prompt': async () => ({ response: 'hello', model: 'gpt-4o' }),
107
+ 'data-create-item': async () => {
108
+ persistCalls.push('persist')
109
+ return { success: true }
110
+ },
111
+ }
112
+
113
+ await utils.executeNodes(nodes, edges, {}, handlers, { nodes, edges }, null, 'exec-2')
114
+ expect(persistCalls).toHaveLength(1)
115
+ })
116
+ })
117
+
118
+ describe('generated server routes gate node results through the shared helpers', () => {
119
+ const segment: WorkflowSegment = {
120
+ id: 'server-1',
121
+ env: 'server',
122
+ nodeIds: ['ai-1'],
123
+ nodes: [
124
+ {
125
+ id: 'ai-1',
126
+ type: 'ai-custom-prompt',
127
+ config: { prompt: 'hi', streaming: true },
128
+ stepNumber: 1,
129
+ } as never,
130
+ ],
131
+ edges: [],
132
+ }
133
+
134
+ it('non-streaming segment route uses isFatalNodeResult', () => {
135
+ const route = generateServerSegmentAPIRoute(
136
+ { ...segment, nodes: [{ ...segment.nodes[0], config: { prompt: 'hi' } } as never] },
137
+ 'Chat'
138
+ )
139
+ expect(route).toContain('utils.isFatalNodeResult(result)')
140
+ expect(route).toContain('utils.fatalNodeResultMessage(result)')
141
+ })
142
+
143
+ it('streaming segment route gates the streaming AI result itself before running on-end nodes', () => {
144
+ const route = generateStreamingServerSegmentAPIRoute(segment, 'Chat')
145
+ // Two gates: the streaming AI node's own result + the non-streaming path.
146
+ const occurrences = route.split('utils.isFatalNodeResult(result)').length - 1
147
+ expect(occurrences).toBeGreaterThanOrEqual(2)
148
+ })
149
+ })
@@ -98,30 +98,30 @@ describe('data-api safeQuery wrapper — defends against Postgres type-coercion
98
98
 
99
99
  describe('data-api safeQuery — runtime semantics', () => {
100
100
  const code = generateDataAPIRoute()
101
- // Eval the safeQuery helper out of the emitted file and verify it
102
- // behaves as we expect against a fake client.
103
- const evalFn = () => {
104
- const startTag = 'async function safeQuery'
105
- const start = code.indexOf(startTag)
106
- // Find the matching closing brace via depth counting
101
+ const extractFunctionBlock = (src: string, tag: string): string => {
102
+ const start = src.indexOf(tag)
107
103
  let depth = 0
108
- let i = code.indexOf('{', start)
109
- for (; i < code.length; i++) {
110
- if (code[i] === '{') {
104
+ let i = src.indexOf('{', start)
105
+ for (; i < src.length; i++) {
106
+ if (src[i] === '{') {
111
107
  depth++
112
- } else if (code[i] === '}') {
108
+ } else if (src[i] === '}') {
113
109
  depth--
114
110
  if (depth === 0) {
115
111
  break
116
112
  }
117
113
  }
118
114
  }
119
- const fnSrc = code.slice(start, i + 1)
120
- // Also extract the SAFE_COERCION_ERROR_CODES constant
121
- const codesStart = code.indexOf('var SAFE_COERCION_ERROR_CODES')
122
- const codesEnd = code.indexOf(';', codesStart)
123
- const codesSrc = code.slice(codesStart, codesEnd + 1)
124
- return new Function(codesSrc + '\n' + fnSrc + '\nreturn safeQuery;')() as any
115
+ return src.slice(start, i + 1)
116
+ }
117
+ // Eval the safeQuery helper out of the emitted file and verify it
118
+ // behaves as we expect against a fake client.
119
+ const evalFn = () => {
120
+ const blockStart = code.indexOf('var SAFE_COERCION_ERROR_CODES')
121
+ const safeQueryStart = code.indexOf('async function safeQuery')
122
+ const preamble = code.slice(blockStart, safeQueryStart)
123
+ const safeQueryFn = extractFunctionBlock(code, 'async function safeQuery')
124
+ return new Function(preamble + safeQueryFn + '\nreturn safeQuery;')() as any
125
125
  }
126
126
  const safeQuery = evalFn()
127
127
 
@@ -0,0 +1,114 @@
1
+ import { NextWorkflowProjectPlugin } from '../src/workflow-project-plugin'
2
+ import { nodeRegistry } from '../src/nodes'
3
+
4
+ /**
5
+ * Regression (Vercel "ReferenceError: state_update_local_state is not defined" at
6
+ * "Collecting page data"): node-handlers-client.js / node-handlers-server.js used
7
+ * to emit separate top-level `async function <name>` declarations plus a
8
+ * `module.exports` map that referenced them by name. That forward-reference shape
9
+ * can dangle in production — a bundler/minifier that drops a single-use
10
+ * declaration, or a handler whose emitted name drifts from its map key, leaves the
11
+ * top-level map referencing an undefined identifier, which throws at MODULE-EVAL.
12
+ * The emitter now inlines each handler as its map value (a named function
13
+ * expression), so a dangling reference is structurally impossible.
14
+ */
15
+ describe('generateNodeHandlerFile — inline handler map (dangling-proof)', () => {
16
+ const plugin = new NextWorkflowProjectPlugin()
17
+ const code: string = (plugin as any).generateNodeHandlerFile(
18
+ new Set(['state-update-local-state', 'toast-show', 'general-extract-form-data']),
19
+ 'client'
20
+ )
21
+
22
+ it('inlines each handler inside a self-invoking function map value (no bare forward reference)', () => {
23
+ // The value is an IIFE that runs the handler (possibly multi-statement) and
24
+ // returns its entry function — never a bare top-level declaration reference.
25
+ expect(code).toMatch(/'state-update-local-state':\s*\(function \(\) \{/)
26
+ expect(code).toMatch(/return state_update_local_state;\s*\}\)\(\)/)
27
+ // The old fragile shape — a bare `'x-y': x_y` identifier reference — must be gone.
28
+ expect(code).not.toMatch(/'state-update-local-state':\s*state_update_local_state\s*[,}]/)
29
+ })
30
+
31
+ it('has no dangling map reference (every value is defined at its own site)', () => {
32
+ const entry = /['"]([a-z0-9]+(?:-[a-z0-9]+)+)['"]\s*:\s*([a-z0-9]+(?:_[a-z0-9]+)+)\b/g
33
+ const dangling: string[] = []
34
+ let m: RegExpExecArray | null = entry.exec(code)
35
+ while (m !== null) {
36
+ const [, key, ident] = m
37
+ if (key.replace(/-/g, '_') === ident && !new RegExp(`function ${ident}\\b`).test(code)) {
38
+ dangling.push(ident)
39
+ }
40
+ m = entry.exec(code)
41
+ }
42
+ expect(dangling).toEqual([])
43
+ })
44
+
45
+ it('evaluates as a CommonJS module without ReferenceError and exposes callable handlers', () => {
46
+ const moduleObj = { exports: {} as Record<string, unknown> }
47
+ // Simulate Next.js module-eval (the "Collecting page data" require). If a map
48
+ // value referenced an undefined identifier this would throw ReferenceError.
49
+ // eslint-disable-next-line no-new-func
50
+ new Function('module', 'exports', code)(moduleObj, moduleObj.exports)
51
+ expect(typeof moduleObj.exports['state-update-local-state']).toBe('function')
52
+ expect(typeof moduleObj.exports['toast-show']).toBe('function')
53
+ expect(typeof moduleObj.exports['general-extract-form-data']).toBe('function')
54
+ })
55
+ })
56
+
57
+ /**
58
+ * Whole-registry contract: EVERY node type must produce a syntactically valid,
59
+ * module-eval-safe handler file in BOTH environments. The narrow 3-handler test
60
+ * above only exercised single-function CLIENT handlers, so it missed the class
61
+ * of handlers that emit MULTIPLE top-level statements — AI nodes
62
+ * (`generateAIProviderUtils() + '\n\n' + <fn>`), `payment-charge-user` and
63
+ * `general-rate-limiter` (function + appended helper declarations). Inlining
64
+ * those as a bare object-literal value produced `'ai-custom-prompt': var … `,
65
+ * a SyntaxError that crashed `require()` of node-handlers-server.js. This test
66
+ * evaluates the generated file for every registry type in both envs — the exact
67
+ * loop that surfaced the regression — so any future multi-statement or
68
+ * name-drifting handler fails CI here instead of at a Vercel build.
69
+ */
70
+ describe('generateNodeHandlerFile — every registry node type is eval-safe in both envs', () => {
71
+ const plugin = new NextWorkflowProjectPlugin()
72
+ const allTypes = Object.keys(nodeRegistry)
73
+
74
+ for (const env of ['client', 'server'] as const) {
75
+ it(`compiles + evaluates a file containing every node type (${env})`, () => {
76
+ const code: string = (plugin as any).generateNodeHandlerFile(new Set(allTypes), env)
77
+ // Empty is legal only if no handler is relevant to this env — never here.
78
+ expect(code.length).toBeGreaterThan(0)
79
+
80
+ const moduleObj = { exports: {} as Record<string, unknown> }
81
+ expect(() => {
82
+ // eslint-disable-next-line no-new-func
83
+ new Function('module', 'exports', code)(moduleObj, moduleObj.exports)
84
+ }).not.toThrow()
85
+
86
+ // Each emitted map value must resolve to a callable handler (the IIFE
87
+ // returned the entry function, not `undefined` from a name drift).
88
+ for (const key of Object.keys(moduleObj.exports)) {
89
+ expect(typeof moduleObj.exports[key]).toBe('function')
90
+ }
91
+ })
92
+
93
+ it(`emits an eval-safe single-type file for each node type individually (${env})`, () => {
94
+ const offenders: string[] = []
95
+ for (const type of allTypes) {
96
+ const code: string = (plugin as any).generateNodeHandlerFile(new Set([type]), env)
97
+ if (!code) {
98
+ continue // handler not relevant to this env — nothing emitted
99
+ }
100
+ try {
101
+ const moduleObj = { exports: {} as Record<string, unknown> }
102
+ // eslint-disable-next-line no-new-func
103
+ new Function('module', 'exports', code)(moduleObj, moduleObj.exports)
104
+ if (typeof moduleObj.exports[type] !== 'function') {
105
+ offenders.push(`${type} (value not callable)`)
106
+ }
107
+ } catch (err) {
108
+ offenders.push(`${type} (${(err as Error).message})`)
109
+ }
110
+ }
111
+ expect(offenders).toEqual([])
112
+ })
113
+ }
114
+ })
@@ -144,27 +144,28 @@ describe('raw-query param binding — generated data-API route binds, never inte
144
144
  })
145
145
 
146
146
  it('handleRawQuery actually passes params to a parameterized client.query at runtime', async () => {
147
- // Eval just the safeQuery helper (as data-api-safe-query.test.ts does) and
148
- // confirm it forwards (sql, params) to client.query verbatim.
149
- const start = code.indexOf('async function safeQuery')
150
- let depth = 0
151
- let i = code.indexOf('{', start)
152
- for (; i < code.length; i++) {
153
- if (code[i] === '{') {
154
- depth++
155
- } else if (code[i] === '}') {
156
- depth--
157
- if (depth === 0) {
158
- break
147
+ const extractFunctionBlock = (src: string, tag: string): string => {
148
+ const start = src.indexOf(tag)
149
+ let depth = 0
150
+ let i = src.indexOf('{', start)
151
+ for (; i < src.length; i++) {
152
+ if (src[i] === '{') {
153
+ depth++
154
+ } else if (src[i] === '}') {
155
+ depth--
156
+ if (depth === 0) {
157
+ break
158
+ }
159
159
  }
160
160
  }
161
+ return src.slice(start, i + 1)
161
162
  }
162
- const fnSrc = code.slice(start, i + 1)
163
- const codesStart = code.indexOf('var SAFE_COERCION_ERROR_CODES')
164
- const codesEnd = code.indexOf(';', codesStart)
165
- const codesSrc = code.slice(codesStart, codesEnd + 1)
163
+ const blockStart = code.indexOf('var SAFE_COERCION_ERROR_CODES')
164
+ const safeQueryStart = code.indexOf('async function safeQuery')
165
+ const preamble = code.slice(blockStart, safeQueryStart)
166
+ const safeQueryFn = extractFunctionBlock(code, 'async function safeQuery')
166
167
  // eslint-disable-next-line @typescript-eslint/no-implied-eval
167
- const safeQuery = new Function(codesSrc + '\n' + fnSrc + '\nreturn safeQuery;')() as any
168
+ const safeQuery = new Function(preamble + safeQueryFn + '\nreturn safeQuery;')() as any
168
169
 
169
170
  const calls: Array<{ sql: string; params: unknown[] }> = []
170
171
  const fakeClient = {