import type { Items, NodeId, PersistedMutableRunState, PersistedRunState, RunCurrentState, RunStopCondition, WorkflowDefinition, } from "@codemation/core"; import { ItemsInputNormalizer, inject } from "@codemation/core"; import { RunIntentService } from "@codemation/core/bootstrap"; import { Engine } from "@codemation/core/bootstrap"; import type { Logger, LoggerFactory } from "../logging/Logger"; import { ApplicationTokens } from "../../applicationTokens"; import type { WorkflowRunRepository } from "../../domain/runs/WorkflowRunRepository"; import type { WorkflowDebuggerOverlayRepository } from "../../domain/workflows/WorkflowDebuggerOverlayRepository"; import type { WorkflowDefinitionRepository } from "../../domain/workflows/WorkflowDefinitionRepository"; import { HandlesCommand } from "../../infrastructure/di/HandlesCommandRegistry"; import { ApplicationRequestError } from "../ApplicationRequestError"; import { CommandHandler } from "../bus/CommandHandler"; import type { CreateRunRequest, RunCommandResult } from "../contracts/RunContracts"; import { WorkflowDebuggerOverlayStateFactory } from "../workflows/WorkflowDebuggerOverlayStateFactory"; import { StartWorkflowRunCommand } from "./StartWorkflowRunCommand"; import { CredentialBindingService } from "../../domain/credentials/CredentialBindingService"; @HandlesCommand.forCommand(StartWorkflowRunCommand) export class StartWorkflowRunCommandHandler extends CommandHandler { private readonly routesLog: Logger; constructor( @inject(Engine) private readonly engine: Engine, @inject(ItemsInputNormalizer) private readonly itemsInputNormalizer: ItemsInputNormalizer, @inject(RunIntentService) private readonly runIntentService: RunIntentService, @inject(ApplicationTokens.WorkflowDefinitionRepository) private readonly workflowDefinitionRepository: WorkflowDefinitionRepository, @inject(ApplicationTokens.WorkflowRunRepository) private readonly workflowRunRepository: WorkflowRunRepository, @inject(ApplicationTokens.WorkflowDebuggerOverlayRepository) private readonly workflowDebuggerOverlayRepository: WorkflowDebuggerOverlayRepository, @inject(CredentialBindingService) private readonly credentialBindingService: CredentialBindingService, @inject(ApplicationTokens.LoggerFactory) loggerFactory: LoggerFactory, ) { super(); this.routesLog = loggerFactory.create("codemation-routes.server"); } async execute(command: StartWorkflowRunCommand): Promise { const body = command.body; if (!body.workflowId) { throw new ApplicationRequestError(400, "Missing workflowId"); } const sourceState = body.sourceRunId && !body.currentState ? await this.workflowRunRepository.load(body.sourceRunId) : undefined; const debuggerOverlay = await this.workflowDebuggerOverlayRepository.load(body.workflowId); const workflow = await this.resolveWorkflow(body); if (!workflow) { throw new ApplicationRequestError(404, "Unknown workflowId"); } await this.credentialBindingService.assertRequiredCredentialsBound(body.workflowId); const executionOptions = body.mode ? { mode: body.mode, sourceWorkflowId: body.workflowId, sourceRunId: body.sourceRunId ?? debuggerOverlay?.copiedFromRunId, derivedFromRunId: body.sourceRunId ?? debuggerOverlay?.copiedFromRunId, } : undefined; const legacyStartNodeId = body.startAt as NodeId | undefined; const clearFromNodeId = body.clearFromNodeId as NodeId | undefined; const requestedItems = this.resolveRequestedItems(body); const items = this.resolveRunRequestItems(workflow, legacyStartNodeId, requestedItems); const currentState = this.createCurrentState({ workflowId: body.workflowId, requestedCurrentState: body.currentState, sourceState, debuggerOverlay, }); const synthesizeTriggerItems = this.resolveSynthesizeTriggerItems({ workflow, mode: body.mode, requestedItems, requestedSynthesizeTriggerItems: body.synthesizeTriggerItems, currentState, }); const result = legacyStartNodeId && this.hasReusableCurrentState(currentState) && !clearFromNodeId ? await this.runIntentService.rerunFromNode({ workflow, nodeId: legacyStartNodeId, currentState, items: requestedItems, synthesizeTriggerItems, executionOptions, workflowSnapshot: sourceState?.workflowSnapshot, mutableState: this.cloneMutableState(currentState.mutableState), }) : await this.runIntentService.startWorkflow({ workflow, startAt: legacyStartNodeId && !body.sourceRunId && !body.stopAt ? legacyStartNodeId : undefined, items, synthesizeTriggerItems, executionOptions, workflowSnapshot: sourceState?.workflowSnapshot, mutableState: this.cloneMutableState(currentState.mutableState), currentState, reset: this.createResetRequest(clearFromNodeId), stopCondition: legacyStartNodeId && !body.sourceRunId && !body.currentState && !body.stopAt ? undefined : this.createStopCondition(body.stopAt), }); const state = (await this.workflowRunRepository.load(result.runId)) ?? null; this.routesLog.info( `postRun workflow=${workflow.id} runId=${result.runId} status=${result.status} persistedStatus=${state?.status ?? "missing"}`, ); return { runId: result.runId, workflowId: result.workflowId, startedAt: result.startedAt, status: result.status, state, }; } private async resolveWorkflow(body: CreateRunRequest): Promise { if (body.currentState) { if (!body.workflowId) { return undefined; } return await this.workflowDefinitionRepository.getDefinition(body.workflowId); } if (body.sourceRunId) { const sourceState = await this.workflowRunRepository.load(body.sourceRunId); if (!sourceState) { return undefined; } return this.engine.resolveWorkflowSnapshot({ workflowId: sourceState.workflowId, workflowSnapshot: sourceState.workflowSnapshot, }); } if (!body.workflowId) { return undefined; } return await this.workflowDefinitionRepository.getDefinition(body.workflowId); } private resolveRunRequestItems(workflow: WorkflowDefinition, startAt: string | undefined, items?: Items): Items { if (items !== undefined) { return items; } return this.isTriggerStart(workflow, startAt) ? [] : [{ json: {} }]; } private resolveRequestedItems(body: CreateRunRequest): Items | undefined { return body.items == null ? undefined : this.itemsInputNormalizer.normalize(body.items); } private isTriggerStart(workflow: WorkflowDefinition, startAt: string | undefined): boolean { const resolvedStartAt = startAt ?? workflow.nodes.find((node) => node.kind === "trigger")?.id ?? workflow.nodes[0]?.id; const startNode = resolvedStartAt ? workflow.nodes.find((node) => node.id === resolvedStartAt) : undefined; return startNode?.kind === "trigger"; } private cloneMutableState(mutableState: PersistedRunState["mutableState"]): PersistedMutableRunState | undefined { if (!mutableState) { return undefined; } return JSON.parse(JSON.stringify(mutableState)) as PersistedMutableRunState; } private cloneRunCurrentState(state: PersistedRunState | undefined): RunCurrentState { if (!state) { return WorkflowDebuggerOverlayStateFactory.cloneCurrentState(undefined); } return { outputsByNode: JSON.parse(JSON.stringify(state.outputsByNode)) as RunCurrentState["outputsByNode"], nodeSnapshotsByNodeId: JSON.parse( JSON.stringify(state.nodeSnapshotsByNodeId), ) as RunCurrentState["nodeSnapshotsByNodeId"], mutableState: this.cloneMutableState(state.mutableState), }; } private createCurrentState( args: Readonly<{ workflowId: string; requestedCurrentState: RunCurrentState | undefined; sourceState: PersistedRunState | undefined; debuggerOverlay: Awaited>; }>, ): RunCurrentState { if (args.requestedCurrentState) { return WorkflowDebuggerOverlayStateFactory.cloneCurrentState(args.requestedCurrentState); } const baseCurrentState = args.sourceState ? this.cloneRunCurrentState(args.sourceState) : WorkflowDebuggerOverlayStateFactory.cloneCurrentState(args.debuggerOverlay?.currentState); if (!args.sourceState || !args.debuggerOverlay || args.debuggerOverlay.workflowId !== args.workflowId) { return baseCurrentState; } return { ...baseCurrentState, mutableState: this.cloneMutableState(args.debuggerOverlay.currentState.mutableState), }; } private createStopCondition(stopAtNodeId: string | undefined): RunStopCondition { if (!stopAtNodeId) { return { kind: "workflowCompleted" }; } return { kind: "nodeCompleted", nodeId: stopAtNodeId as NodeId, }; } private createResetRequest(clearFromNodeId: NodeId | undefined): Readonly<{ clearFromNodeId: NodeId }> | undefined { if (!clearFromNodeId) { return undefined; } return { clearFromNodeId, }; } private hasReusableCurrentState(currentState: RunCurrentState): boolean { return ( Object.keys(currentState.outputsByNode).length > 0 || Object.keys(currentState.nodeSnapshotsByNodeId).length > 0 || Object.keys(currentState.mutableState?.nodesById ?? {}).length > 0 ); } private resolveSynthesizeTriggerItems( args: Readonly<{ workflow: WorkflowDefinition; mode: CreateRunRequest["mode"]; requestedItems: Items | undefined; requestedSynthesizeTriggerItems: boolean | undefined; currentState: RunCurrentState; }>, ): boolean { if (this.hasNonEmptyItems(args.requestedItems)) { return false; } if (args.requestedSynthesizeTriggerItems === true) { return true; } if (args.mode !== "manual") { return args.requestedSynthesizeTriggerItems ?? false; } if (!this.workflowHasTrigger(args.workflow)) { return args.requestedSynthesizeTriggerItems ?? false; } if (this.currentStateHasTriggerData(args.workflow, args.currentState)) { return args.requestedSynthesizeTriggerItems ?? false; } return true; } private workflowHasTrigger(workflow: WorkflowDefinition): boolean { return workflow.nodes.some((node) => node.kind === "trigger"); } private currentStateHasTriggerData(workflow: WorkflowDefinition, currentState: RunCurrentState): boolean { const triggerNodeIds = workflow.nodes.filter((node) => node.kind === "trigger").map((node) => node.id); for (const triggerNodeId of triggerNodeIds) { if (this.hasOutputItems(currentState.outputsByNode[triggerNodeId])) { return true; } if (this.hasOutputItems(currentState.mutableState?.nodesById?.[triggerNodeId]?.pinnedOutputsByPort)) { return true; } } return false; } private hasOutputItems(outputsByPort: Readonly>> | undefined): boolean { if (!outputsByPort) { return false; } return Object.values(outputsByPort).some((items) => this.hasNonEmptyItems(items)); } private hasNonEmptyItems(items: Items | undefined): boolean { return (items?.length ?? 0) > 0; } }