Skip to content

Commit c853fde

Browse files
committed
merge: main into mcp-tool-mode
Signed-off-by: betterclever <paliwal.pranjal83@gmail.com>
2 parents 7d7836b + 38492dc commit c853fde

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+4053
-2774
lines changed

backend/src/workflows/workflows.service.ts

Lines changed: 149 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
BadRequestException,
99
} from '@nestjs/common';
1010
import { status as grpcStatus, type ServiceError } from '@grpc/grpc-js';
11+
import { z } from 'zod';
1112

1213
import { compileWorkflowGraph } from '../dsl/compiler';
1314
// Ensure all worker components are registered before accessing the registry
@@ -21,6 +22,8 @@ import {
2122
import {
2223
WorkflowGraphDto,
2324
WorkflowGraphSchema,
25+
WorkflowNodeSchema,
26+
WorkflowNodeDataSchema,
2427
ServiceWorkflowResponse,
2528
UpdateWorkflowMetadataDto,
2629
} from './dto/workflow-graph.dto';
@@ -40,7 +43,7 @@ import {
4043
ExecutionInputPreview,
4144
ExecutionTriggerMetadata,
4245
} from '@shipsec/shared';
43-
import type { WorkflowRunRecord, WorkflowVersionRecord } from '../database/schema';
46+
import type { WorkflowRunRecord, WorkflowVersionRecord, WorkflowGraph } from '../database/schema';
4447
import type { AuthContext } from '../auth/types';
4548

4649
export interface WorkflowRunRequest {
@@ -366,13 +369,155 @@ export class WorkflowsService {
366369
record: WorkflowRecord,
367370
version?: WorkflowVersionRecord | null,
368371
): ServiceWorkflowResponse {
372+
// Resolve dynamic ports for the graph so Entry Point nodes show correct outputs
373+
const resolvedGraph = this.resolveGraphPorts(record.graph);
374+
369375
return {
370376
...record,
377+
graph: resolvedGraph,
371378
currentVersionId: version?.id ?? null,
372379
currentVersion: version?.version ?? null,
373380
};
374381
}
375382

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+
376521
async delete(id: string, auth?: AuthContext | null): Promise<void> {
377522
const organizationId = await this.requireWorkflowAdmin(id, auth);
378523
await this.repository.delete(id, { organizationId });
@@ -1213,80 +1358,11 @@ export class WorkflowsService {
12131358
return 0;
12141359
}
12151360

1216-
private parse(dto: WorkflowGraphDto) {
1361+
private parse(dto: WorkflowGraphDto): WorkflowGraph {
12171362
const parsed = WorkflowGraphSchema.parse(dto);
12181363

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-
};
1364+
// Resolve dynamic ports for all nodes using the shared helper
1365+
return this.resolveGraphPorts(parsed);
12901366
}
12911367

12921368
private formatInputSummary(inputs?: Record<string, unknown>): string {

0 commit comments

Comments
 (0)