88 BadRequestException ,
99} from '@nestjs/common' ;
1010import { status as grpcStatus , type ServiceError } from '@grpc/grpc-js' ;
11- import { z } from 'zod' ;
1211
1312import { compileWorkflowGraph } from '../dsl/compiler' ;
1413// Ensure all worker components are registered before accessing the registry
@@ -22,8 +21,6 @@ import {
2221import {
2322 WorkflowGraphDto ,
2423 WorkflowGraphSchema ,
25- WorkflowNodeSchema ,
26- WorkflowNodeDataSchema ,
2724 ServiceWorkflowResponse ,
2825 UpdateWorkflowMetadataDto ,
2926} from './dto/workflow-graph.dto' ;
@@ -43,7 +40,7 @@ import {
4340 ExecutionInputPreview ,
4441 ExecutionTriggerMetadata ,
4542} from '@shipsec/shared' ;
46- import type { WorkflowRunRecord , WorkflowVersionRecord , WorkflowGraph } from '../database/schema' ;
43+ import type { WorkflowRunRecord , WorkflowVersionRecord } from '../database/schema' ;
4744import type { AuthContext } from '../auth/types' ;
4845
4946export interface WorkflowRunRequest {
@@ -369,155 +366,13 @@ export class WorkflowsService {
369366 record : WorkflowRecord ,
370367 version ?: WorkflowVersionRecord | null ,
371368 ) : ServiceWorkflowResponse {
372- // Resolve dynamic ports for the graph so Entry Point nodes show correct outputs
373- const resolvedGraph = this . resolveGraphPorts ( record . graph ) ;
374-
375369 return {
376370 ...record ,
377- graph : resolvedGraph ,
378371 currentVersionId : version ?. id ?? null ,
379372 currentVersion : version ?. version ?? null ,
380373 } ;
381374 }
382375
383- /**
384- * Extract component parameters from node data, handling legacy schema formats.
385- * This handles the migration from old formats where params might be at:
386- * - nodeData.config.params (current schema)
387- * - nodeData.parameters (legacy)
388- * - nodeData.config (legacy - when config was the params object directly)
389- */
390- private extractNodeParams (
391- nodeData : z . infer < typeof WorkflowNodeDataSchema > ,
392- ) : Record < string , unknown > {
393- // Current schema: params are in config.params
394- if ( nodeData . config ?. params && Object . keys ( nodeData . config . params ) . length > 0 ) {
395- return nodeData . config . params ;
396- }
397-
398- // Legacy: params stored directly on nodeData (via extended properties)
399- const extendedNodeData = nodeData as Record < string , unknown > ;
400- if ( extendedNodeData . parameters && typeof extendedNodeData . parameters === 'object' ) {
401- return extendedNodeData . parameters as Record < string , unknown > ;
402- }
403-
404- // Legacy: config was the params object itself (before nested config.params structure)
405- // Only use this if config doesn't have the modern structure
406- if (
407- nodeData . config &&
408- ! ( 'params' in nodeData . config ) &&
409- ! ( 'inputOverrides' in nodeData . config ) &&
410- typeof nodeData . config === 'object'
411- ) {
412- return nodeData . config as Record < string , unknown > ;
413- }
414-
415- return { } ;
416- }
417-
418- /**
419- * Extract component ID from node, handling frontend extensions.
420- * The componentId might be in node.type or in extended nodeData properties.
421- */
422- private extractComponentId (
423- node : z . infer < typeof WorkflowNodeSchema > ,
424- nodeData : z . infer < typeof WorkflowNodeDataSchema > ,
425- ) : string | null {
426- // In backend schema, node.type contains the component ID
427- if ( node . type && node . type !== 'workflow' ) {
428- return node . type ;
429- }
430-
431- // Frontend extensions might store componentId/componentSlug in nodeData
432- const extendedNodeData = nodeData as Record < string , unknown > ;
433- if ( typeof extendedNodeData . componentId === 'string' ) {
434- return extendedNodeData . componentId ;
435- }
436- if ( typeof extendedNodeData . componentSlug === 'string' ) {
437- return extendedNodeData . componentSlug ;
438- }
439-
440- return null ;
441- }
442-
443- /**
444- * Resolve dynamic ports for a single node based on its component and parameters.
445- */
446- private resolveNodePorts (
447- node : z . infer < typeof WorkflowNodeSchema > ,
448- ) : z . infer < typeof WorkflowNodeSchema > {
449- const nodeData = node . data ;
450- const componentId = this . extractComponentId ( node , nodeData ) ;
451-
452- if ( ! componentId ) {
453- return node ;
454- }
455-
456- try {
457- const entry = componentRegistry . getMetadata ( componentId ) ;
458- if ( ! entry ) {
459- return node ;
460- }
461- const component = entry . definition ;
462- const baseInputs = entry . inputs ?? extractPorts ( component . inputs ) ;
463- const baseOutputs = entry . outputs ?? extractPorts ( component . outputs ) ;
464-
465- const params = this . extractNodeParams ( nodeData ) ;
466-
467- if ( typeof component . resolvePorts === 'function' ) {
468- try {
469- const resolved = component . resolvePorts ( params ) ;
470- return {
471- ...node ,
472- data : {
473- ...nodeData ,
474- dynamicInputs : resolved . inputs ? extractPorts ( resolved . inputs ) : baseInputs ,
475- dynamicOutputs : resolved . outputs ? extractPorts ( resolved . outputs ) : baseOutputs ,
476- } ,
477- } ;
478- } catch ( resolveError ) {
479- this . logger . warn ( `Failed to resolve ports for component ${ componentId } : ${ resolveError } ` ) ;
480- return {
481- ...node ,
482- data : {
483- ...nodeData ,
484- dynamicInputs : baseInputs ,
485- dynamicOutputs : baseOutputs ,
486- } ,
487- } ;
488- }
489- } else {
490- return {
491- ...node ,
492- data : {
493- ...nodeData ,
494- dynamicInputs : baseInputs ,
495- dynamicOutputs : baseOutputs ,
496- } ,
497- } ;
498- }
499- } catch ( error ) {
500- this . logger . warn ( `Failed to get component ${ componentId } for port resolution: ${ error } ` ) ;
501- return node ;
502- }
503- }
504-
505- /**
506- * Resolve dynamic ports for all nodes in a workflow graph.
507- * This ensures Entry Point nodes and other components with resolvePorts
508- * have their dynamicInputs/dynamicOutputs populated correctly.
509- */
510- private resolveGraphPorts ( graph : WorkflowGraph ) : WorkflowGraph {
511- if ( ! graph || ! Array . isArray ( graph . nodes ) ) {
512- return graph ;
513- }
514-
515- return {
516- ...graph ,
517- nodes : graph . nodes . map ( ( node ) => this . resolveNodePorts ( node ) ) ,
518- } ;
519- }
520-
521376 async delete ( id : string , auth ?: AuthContext | null ) : Promise < void > {
522377 const organizationId = await this . requireWorkflowAdmin ( id , auth ) ;
523378 await this . repository . delete ( id , { organizationId } ) ;
@@ -1358,11 +1213,80 @@ export class WorkflowsService {
13581213 return 0 ;
13591214 }
13601215
1361- private parse ( dto : WorkflowGraphDto ) : WorkflowGraph {
1216+ private parse ( dto : WorkflowGraphDto ) {
13621217 const parsed = WorkflowGraphSchema . parse ( dto ) ;
13631218
1364- // Resolve dynamic ports for all nodes using the shared helper
1365- return this . resolveGraphPorts ( parsed ) ;
1219+ // Resolve dynamic ports for each node based on its component and parameters
1220+ const nodesWithResolvedPorts = parsed . nodes . map ( ( node ) => {
1221+ const nodeData = node . data as any ;
1222+ // Component ID can be in node.type, data.componentId, or data.componentSlug
1223+ // In the workflow graph schema, node.type contains the component ID (e.g., "security.virustotal.lookup")
1224+ const componentId =
1225+ node . type !== 'workflow' ? node . type : nodeData . componentId || nodeData . componentSlug ;
1226+
1227+ if ( ! componentId ) {
1228+ return node ;
1229+ }
1230+
1231+ try {
1232+ const entry = componentRegistry . getMetadata ( componentId ) ;
1233+ if ( ! entry ) {
1234+ return node ;
1235+ }
1236+ const component = entry . definition ;
1237+ const baseInputs = entry . inputs ?? extractPorts ( component . inputs ) ;
1238+ const baseOutputs = entry . outputs ?? extractPorts ( component . outputs ) ;
1239+
1240+ // Get parameters from node data (they may be stored in config.params, parameters, or at data level)
1241+ const params = nodeData . parameters || nodeData . config ?. params || nodeData . config || { } ;
1242+
1243+ // Resolve ports using the component's resolvePorts function if available
1244+ if ( typeof component . resolvePorts === 'function' ) {
1245+ try {
1246+ const resolved = component . resolvePorts ( params ) ;
1247+ return {
1248+ ...node ,
1249+ data : {
1250+ ...nodeData ,
1251+ dynamicInputs : resolved . inputs ? extractPorts ( resolved . inputs ) : baseInputs ,
1252+ dynamicOutputs : resolved . outputs ? extractPorts ( resolved . outputs ) : baseOutputs ,
1253+ } ,
1254+ } ;
1255+ } catch ( resolveError ) {
1256+ this . logger . warn (
1257+ `Failed to resolve ports for component ${ componentId } : ${ resolveError } ` ,
1258+ ) ;
1259+ // Fall back to static metadata
1260+ return {
1261+ ...node ,
1262+ data : {
1263+ ...nodeData ,
1264+ dynamicInputs : baseInputs ,
1265+ dynamicOutputs : baseOutputs ,
1266+ } ,
1267+ } ;
1268+ }
1269+ } else {
1270+ // No dynamic resolver, use static metadata
1271+ return {
1272+ ...node ,
1273+ data : {
1274+ ...nodeData ,
1275+ dynamicInputs : baseInputs ,
1276+ dynamicOutputs : baseOutputs ,
1277+ } ,
1278+ } ;
1279+ }
1280+ } catch ( error ) {
1281+ this . logger . warn ( `Failed to get component ${ componentId } for port resolution: ${ error } ` ) ;
1282+ return node ;
1283+ }
1284+ } ) ;
1285+
1286+ return {
1287+ ...parsed ,
1288+ nodes : nodesWithResolvedPorts ,
1289+ } ;
13661290 }
13671291
13681292 private formatInputSummary ( inputs ?: Record < string , unknown > ) : string {
0 commit comments