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

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 (48) 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.map +1 -1
  22. package/dist/cjs/workflow-project-plugin.js +40 -5
  23. package/dist/cjs/workflow-project-plugin.js.map +1 -1
  24. package/dist/esm/api-route-generator.d.ts.map +1 -1
  25. package/dist/esm/api-route-generator.js +14 -8
  26. package/dist/esm/api-route-generator.js.map +1 -1
  27. package/dist/esm/data-api-route-generator.d.ts.map +1 -1
  28. package/dist/esm/data-api-route-generator.js +13 -2
  29. package/dist/esm/data-api-route-generator.js.map +1 -1
  30. package/dist/esm/executor-generator.d.ts.map +1 -1
  31. package/dist/esm/executor-generator.js +47 -5
  32. package/dist/esm/executor-generator.js.map +1 -1
  33. package/dist/esm/nodes/general/general-custom-js.d.ts.map +1 -1
  34. package/dist/esm/nodes/general/general-custom-js.js +7 -0
  35. package/dist/esm/nodes/general/general-custom-js.js.map +1 -1
  36. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  37. package/dist/esm/workflow-component-plugin.js +9 -3
  38. package/dist/esm/workflow-component-plugin.js.map +1 -1
  39. package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
  40. package/dist/esm/workflow-project-plugin.js +40 -5
  41. package/dist/esm/workflow-project-plugin.js.map +1 -1
  42. package/package.json +2 -2
  43. package/src/api-route-generator.ts +14 -8
  44. package/src/data-api-route-generator.ts +13 -2
  45. package/src/executor-generator.ts +47 -5
  46. package/src/nodes/general/general-custom-js.ts +8 -0
  47. package/src/workflow-component-plugin.ts +15 -10
  48. package/src/workflow-project-plugin.ts +39 -4
@@ -159,12 +159,23 @@ function validateFilters(filters) {
159
159
  // 22003 (numeric value out of range), 22023 (invalid parameter value).
160
160
  // We pass the original query / params back to the caller as a debug
161
161
  // label so log output stays diagnosable.
162
- var SAFE_COERCION_ERROR_CODES = { '22P02': 1, '22008': 1, '22003': 1, '22023': 1 };
162
+ // Also covers 22007 (invalid_datetime_format an empty "Filter by date" value
163
+ // hitting a date column: invalid input syntax for type date). Beyond the code
164
+ // list we ALSO suppress ANY "invalid input syntax for type" message: every one
165
+ // means the bound value could never coerce to the column type, so no row can
166
+ // match and an empty result is the correct semantics — never a 500 the user sees
167
+ // as "Failed to update. Please try again".
168
+ var SAFE_COERCION_ERROR_CODES = { '22P02': 1, '22007': 1, '22008': 1, '22003': 1, '22023': 1 };
169
+ function isSafeCoercionError(err) {
170
+ if (!err) return false;
171
+ if (SAFE_COERCION_ERROR_CODES[err.code]) return true;
172
+ return typeof err.message === 'string' && /invalid input syntax for type/i.test(err.message);
173
+ }
163
174
  async function safeQuery(client, sql, params, mode) {
164
175
  try {
165
176
  return await client.query(sql, params);
166
177
  } catch (err) {
167
- if (err && SAFE_COERCION_ERROR_CODES[err.code]) {
178
+ if (isSafeCoercionError(err)) {
168
179
  console.warn('[data-api] suppressed Postgres coercion error (' + err.code + '): ' + (err.message || err) +
169
180
  ' — returning empty result for ' + (mode || 'query') + '. SQL=' + sql + ' params=' + JSON.stringify(params));
170
181
  if (mode === 'count') {
@@ -586,6 +586,24 @@ function topoSortNodes(nodes, edges) {
586
586
  return sorted;
587
587
  }
588
588
 
589
+ // A node result signals failure either through the legacy string contract
590
+ // ({ error: '...' } / { success: false }) or through the AI-node contract
591
+ // ({ error: true, message, code } — provider/auth failures). Both must halt
592
+ // the workflow; treating error:true as success used to let the pipeline limp
593
+ // into downstream NOT NULL violations.
594
+ function isFatalNodeResult(result) {
595
+ if (!result) return false;
596
+ return result.success === false ||
597
+ (typeof result.error === 'string' && result.error) ||
598
+ result.error === true;
599
+ }
600
+
601
+ function fatalNodeResultMessage(result) {
602
+ if (typeof result.error === 'string' && result.error) return result.error;
603
+ if (typeof result.message === 'string' && result.message) return result.message;
604
+ return 'Node execution failed';
605
+ }
606
+
589
607
  async function executeNodes(nodes, edges, context, nodeHandlers, workflowConfig, callServerSegment, executionId) {
590
608
  const executed = {};
591
609
  const sortedNodes = topoSortNodes(nodes, edges);
@@ -684,6 +702,9 @@ async function executeNodes(nodes, edges, context, nodeHandlers, workflowConfig,
684
702
  await executeNodes(onStreamNodes, edges, context, nodeHandlers, workflowConfig, callServerSegment, executionId);
685
703
  }
686
704
  });
705
+ if (isFatalNodeResult(streamResult)) {
706
+ throw new Error(fatalNodeResultMessage(streamResult));
707
+ }
687
708
  context[node.id] = streamResult;
688
709
 
689
710
  if (onEndNodes.length > 0) {
@@ -726,8 +747,8 @@ async function executeNodes(nodes, edges, context, nodeHandlers, workflowConfig,
726
747
  throw earlyErr;
727
748
  }
728
749
 
729
- if (result && (result.success === false || (typeof result.error === 'string' && result.error))) {
730
- throw new Error(result.error || 'Node execution failed');
750
+ if (isFatalNodeResult(result)) {
751
+ throw new Error(fatalNodeResultMessage(result));
731
752
  }
732
753
 
733
754
  context[node.id] = result;
@@ -1006,7 +1027,9 @@ module.exports = {
1006
1027
  executeParallel,
1007
1028
  collectBranchNodes,
1008
1029
  markAllBranchNodes,
1009
- isStreamingAINode
1030
+ isStreamingAINode,
1031
+ isFatalNodeResult,
1032
+ fatalNodeResultMessage
1010
1033
  };
1011
1034
  `
1012
1035
  }
@@ -1200,6 +1223,19 @@ function findStreamingAINodes(allNodes, allEdges) {
1200
1223
  return map;
1201
1224
  }
1202
1225
 
1226
+ // A streaming AI node's on-stream / on-end branches are collected from the
1227
+ // FULL workflow node list, so they may contain server nodes (e.g. the SQL
1228
+ // insert persisting a chat answer). Those already execute inside their own
1229
+ // server segments with full configs; the browser bundle only ships their
1230
+ // config redacted down to the client-safe whitelist (segment-splitter's
1231
+ // redactServerNodeConfig), so executing them here with CLIENT handlers both
1232
+ // double-executes them and crashes on the missing config. Emitted nodes carry
1233
+ // executionEnv for exactly this filter; nodes without the marker (older
1234
+ // bundles) keep executing client-side as before.
1235
+ function clientExecutableBranchNodes(branchNodes) {
1236
+ return branchNodes.filter(function(n) { return !n || n.executionEnv !== 'server'; });
1237
+ }
1238
+
1203
1239
  async function callStreamingServerSegment(segmentUrl, context, streamingInfo, allNodes, allEdges, clientHandlers, workflowConfig, executionId) {
1204
1240
  const prunedContext = pruneContext(context);
1205
1241
  const handledNodeIds = {};
@@ -1254,7 +1290,10 @@ async function callStreamingServerSegment(segmentUrl, context, streamingInfo, al
1254
1290
  context[data.nodeId] = { chunk: data.chunk, fullResponse: data.fullResponse, model: data.model };
1255
1291
  const info = streamingInfo[data.nodeId];
1256
1292
  if (info && info.onStreamNodes.length > 0) {
1257
- await utils.executeNodes(info.onStreamNodes, allEdges, context, clientHandlers, workflowConfig, null, executionId);
1293
+ const runnableStreamNodes = clientExecutableBranchNodes(info.onStreamNodes);
1294
+ if (runnableStreamNodes.length > 0) {
1295
+ await utils.executeNodes(runnableStreamNodes, allEdges, context, clientHandlers, workflowConfig, null, executionId);
1296
+ }
1258
1297
  for (let sni = 0; sni < info.onStreamNodes.length; sni++) {
1259
1298
  handledNodeIds[info.onStreamNodes[sni].id] = true;
1260
1299
  }
@@ -1269,7 +1308,10 @@ async function callStreamingServerSegment(segmentUrl, context, streamingInfo, al
1269
1308
  for (let sk = 0; sk < streamedKeys.length; sk++) {
1270
1309
  const endInfo = streamingInfo[streamedKeys[sk]];
1271
1310
  if (endInfo && endInfo.onEndNodes.length > 0) {
1272
- await utils.executeNodes(endInfo.onEndNodes, allEdges, context, clientHandlers, workflowConfig, null, executionId);
1311
+ const runnableEndNodes = clientExecutableBranchNodes(endInfo.onEndNodes);
1312
+ if (runnableEndNodes.length > 0) {
1313
+ await utils.executeNodes(runnableEndNodes, allEdges, context, clientHandlers, workflowConfig, null, executionId);
1314
+ }
1273
1315
  for (let eni = 0; eni < endInfo.onEndNodes.length; eni++) {
1274
1316
  handledNodeIds[endInfo.onEndNodes[eni].id] = true;
1275
1317
  }
@@ -23,6 +23,14 @@ import { NodeHandlerGenerator, handlerToString } from '../types'
23
23
  async function general_custom_js(config: any, context: Record<string, unknown>) {
24
24
  const code = config.code
25
25
 
26
+ // A server-classified custom-js node's config is redacted to the client-safe
27
+ // whitelist ({} here) before it ships to the browser. If such a node ever
28
+ // reaches a client handler (defense-in-depth — the runtime filters them),
29
+ // no-op cleanly instead of crashing the whole workflow on `code.length`.
30
+ if (typeof code !== 'string' || code.length === 0) {
31
+ return {}
32
+ }
33
+
26
34
  // Reserved context keys that must never leak into params / innerParams.
27
35
  // Anything starting with "__" is internal scaffolding; these named entries
28
36
  // are existing well-known runtime keys.
@@ -1308,16 +1308,21 @@ function __normalizeAdminFormRow(row, defaults) {
1308
1308
  }))
1309
1309
  )
1310
1310
  const nodesJson = JSON.stringify(
1311
- wf.nodes.map((n: UIDLWorkflowNode) => ({
1312
- id: n.id,
1313
- type: n.type,
1314
- config: redactServerNodeConfig(
1315
- n.config,
1316
- nodeEnvById.get(n.id) ?? resolveNodeExecutionEnv(n)
1317
- ),
1318
- stepNumber: n.stepNumber,
1319
- label: n.label,
1320
- }))
1311
+ wf.nodes.map((n: UIDLWorkflowNode) => {
1312
+ // Runtime marker consumed by clientExecutableBranchNodes (client
1313
+ // runtime): a streaming AI node's on-stream/on-end branches are
1314
+ // collected from this full node list, and server nodes in them must
1315
+ // never execute with client handlers (their config is redacted below).
1316
+ const env = nodeEnvById.get(n.id) ?? resolveNodeExecutionEnv(n)
1317
+ return {
1318
+ id: n.id,
1319
+ type: n.type,
1320
+ config: redactServerNodeConfig(n.config, env),
1321
+ stepNumber: n.stepNumber,
1322
+ label: n.label,
1323
+ executionEnv: env,
1324
+ }
1325
+ })
1321
1326
  )
1322
1327
  const edgesJson = JSON.stringify(
1323
1328
  wf.edges.map((e: UIDLWorkflowEdge) => ({
@@ -887,14 +887,45 @@ export class NextWorkflowProjectPlugin implements ProjectPlugin {
887
887
  return ''
888
888
  }
889
889
 
890
- const exports = exportNames.map((t) => ` '${t}': ${t.replace(/-/g, '_')}`).join(',\n')
890
+ // Inline each handler as its `module.exports` map value inside a
891
+ // self-invoking function that returns the handler's entry-point, rather
892
+ // than emitting separate top-level `async function <name>` declarations
893
+ // plus a map that references them by name. `handlers` and `exportNames`
894
+ // are populated together in the loop above, so `handlers[index]` is
895
+ // exactly the handler for `exportNames[index]`.
896
+ //
897
+ // Why the IIFE (not a bare inline expression): a handler is NOT always a
898
+ // single function expression. AI nodes emit shared provider utils
899
+ // (`var __ai_… = …`) BEFORE their entry function, and payment/rate-limiter
900
+ // nodes append helper functions AFTER it. Splicing such a multi-statement
901
+ // string directly as an object-literal value (`'ai-custom-prompt': var … `)
902
+ // is a SyntaxError. Wrapping in `(function () { <handler>; return <fn>; })()`
903
+ // runs every statement in an isolated scope and yields the entry function as
904
+ // the value — so single- AND multi-statement handlers are both valid.
905
+ //
906
+ // Why not top-level declarations + a name reference: that forward-reference
907
+ // shape is fragile in production. If a bundler/minifier drops a single-use
908
+ // declaration (or a handler's emitted name ever drifts from its
909
+ // `nodeType`-derived key), the map is left referencing an undefined
910
+ // identifier — `ReferenceError: <name> is not defined` at MODULE-EVAL time,
911
+ // which fails the Next.js/Vercel build during "Collecting page data". The
912
+ // returned identifier is derived from the same `t` that keys the map and is
913
+ // defined by the handler string right above the `return`, so there is no
914
+ // cross-statement forward reference to dangle. The entry-function naming
915
+ // contract (`handlerToString(fn)` preserves `fn.name`, and every handler
916
+ // names its entry `nodeType.replace(/-/g, '_')`) is enforced by
917
+ // node-handler-file-inline-map.test.ts across the whole node registry.
918
+ const entries = exportNames
919
+ .map((t, index) => {
920
+ const entryFn = t.replace(/-/g, '_')
921
+ return ` '${t}': (function () {\n${handlers[index].trim()}\nreturn ${entryFn};\n})()`
922
+ })
923
+ .join(',\n')
891
924
 
892
925
  return `// Auto-generated workflow node handlers (${env})
893
926
 
894
- ${handlers.join('\n\n')}
895
-
896
927
  module.exports = {
897
- ${exports}
928
+ ${entries}
898
929
  };
899
930
  `
900
931
  }
@@ -972,6 +1003,10 @@ ${exports}
972
1003
  (cn.nodes || []).map((n: any) => ({
973
1004
  ...n,
974
1005
  config: redactServerNodeConfig(n.config, resolveNodeExecutionEnv(n)),
1006
+ // Runtime marker consumed by clientExecutableBranchNodes (client
1007
+ // runtime) so streaming on-stream/on-end branches never execute
1008
+ // server nodes (whose config was just redacted) client-side.
1009
+ executionEnv: resolveNodeExecutionEnv(n),
975
1010
  }))
976
1011
  )
977
1012
  const edgesJson = JSON.stringify(cn.edges)