Some checks failed
Deploy Nyla Frontend to Integration / deploy (push) Failing after 51s
1193 lines
34 KiB
TypeScript
1193 lines
34 KiB
TypeScript
/**
|
|
* Workflow Automation API (mandate-scoped)
|
|
*
|
|
* Replaces instance-scoped /api/workflows/{instanceId}/...
|
|
* with the unified /api/workflow-automation/... base path.
|
|
*
|
|
* Also unifies the former /api/system/workflow-runs/... and
|
|
* /api/automations/runs/... endpoints under the same base.
|
|
*/
|
|
|
|
import type { ApiRequestOptions } from '../hooks/useApi';
|
|
|
|
const LOG = '[WorkflowAutomation]';
|
|
const BASE = '/api/workflow-automation';
|
|
|
|
// ============================================================================
|
|
// TYPES
|
|
// ============================================================================
|
|
|
|
export interface NodeTypeParameter {
|
|
name: string;
|
|
type: string;
|
|
required?: boolean;
|
|
description?: string;
|
|
default?: unknown;
|
|
frontendType?: string;
|
|
frontendOptions?: Record<string, unknown>;
|
|
options?: unknown[];
|
|
validation?: Record<string, unknown>;
|
|
}
|
|
|
|
export interface PortField {
|
|
name: string;
|
|
type: string;
|
|
/** Plain string or per-language map from the API catalog. */
|
|
description: string | Record<string, string>;
|
|
required: boolean;
|
|
enumValues?: string[] | null;
|
|
/** When true, surface at the top of the DataPicker as the most common/recommended pick. */
|
|
recommended?: boolean;
|
|
/** Human label from portTypeCatalog (backend). Preferred over technical path in DataPicker. */
|
|
pickerLabel?: string | null;
|
|
/** Backend: segment for one list element (between List field and nested field). */
|
|
pickerItemLabel?: string | null;
|
|
}
|
|
|
|
export interface PortSchema {
|
|
name: string;
|
|
fields: PortField[];
|
|
}
|
|
|
|
/** One pickable binding — defined on ``outputPorts[n].dataPickOptions`` (authoritative list from gateway). */
|
|
export interface DataPickOption {
|
|
path: (string | number)[];
|
|
pickerLabel: string;
|
|
detail?: string;
|
|
recommended?: boolean;
|
|
iterable?: boolean;
|
|
/** For display and optional strict compatibility (e.g. str, Any). */
|
|
type?: string;
|
|
}
|
|
|
|
/** @deprecated Prefer ``outputPorts[].dataPickOptions``; kept for older payloads. */
|
|
export type OutputPickHint = DataPickOption;
|
|
|
|
export interface InputPortDef {
|
|
accepts: string[];
|
|
}
|
|
|
|
/** Graph-defined output schema (e.g. form fields from node parameters). */
|
|
export interface GraphDefinedSchemaRef {
|
|
kind: 'fromGraph';
|
|
parameter: string;
|
|
}
|
|
|
|
export interface OutputPortDef {
|
|
schema: string | GraphDefinedSchemaRef;
|
|
dynamic?: boolean;
|
|
deriveFrom?: string;
|
|
/**
|
|
* When set, DataPicker uses **only** this list for that port (no portTypeCatalog expansion).
|
|
* Authoritative, like `parameters` for node configuration.
|
|
*/
|
|
dataPickOptions?: DataPickOption[];
|
|
}
|
|
|
|
export interface NodeType {
|
|
id: string;
|
|
category: string;
|
|
label: string;
|
|
description: string;
|
|
parameters: NodeTypeParameter[];
|
|
inputs: number;
|
|
outputs: number;
|
|
outputLabels?: string[];
|
|
executor: string;
|
|
inputPorts?: Record<number, InputPortDef>;
|
|
outputPorts?: Record<number, OutputPortDef>;
|
|
meta?: {
|
|
icon?: string;
|
|
color?: string;
|
|
/** True if this node performs an LLM / AI call (credits). */
|
|
usesAi?: boolean;
|
|
method?: string;
|
|
action?: string;
|
|
};
|
|
}
|
|
export interface NodeTypeCategory {
|
|
id: string;
|
|
label: Record<string, string> | string;
|
|
}
|
|
|
|
export interface SystemVariable {
|
|
type: string;
|
|
description: string;
|
|
}
|
|
|
|
/** Single form field type with its canonical port primitive. Delivered by GET /node-types. */
|
|
export interface FormFieldType {
|
|
id: string;
|
|
label: string;
|
|
portType: string;
|
|
}
|
|
|
|
export interface ConditionOperatorDef {
|
|
id: string;
|
|
label: string;
|
|
labelKey?: string;
|
|
needsValue: boolean;
|
|
valueInput?: { kind: string; options?: string[] };
|
|
}
|
|
|
|
export interface NodeTypesResponse {
|
|
nodeTypes: NodeType[];
|
|
categories: NodeTypeCategory[];
|
|
portTypeCatalog?: Record<string, PortSchema>;
|
|
conditionOperatorCatalog?: Record<string, ConditionOperatorDef[]>;
|
|
systemVariables?: Record<string, SystemVariable>;
|
|
formFieldTypes?: FormFieldType[];
|
|
}
|
|
|
|
export interface WorkflowGraphNode {
|
|
id: string;
|
|
type: string;
|
|
parameters?: Record<string, unknown>;
|
|
inputPorts?: Array<{ name: string; schema: string; accepts?: string[] }>;
|
|
outputPorts?: Array<{ name: string; schema: string | GraphDefinedSchemaRef }>;
|
|
}
|
|
|
|
export interface WorkflowConnection {
|
|
source: string;
|
|
target: string;
|
|
sourceOutput?: number;
|
|
targetInput?: number;
|
|
}
|
|
|
|
export interface WorkflowGraph {
|
|
nodes: WorkflowGraphNode[];
|
|
connections: WorkflowConnection[];
|
|
}
|
|
|
|
export interface ExecuteGraphResponse {
|
|
success: boolean;
|
|
nodeOutputs?: Record<string, unknown>;
|
|
error?: string;
|
|
/** Soft, non-blocking message displayed alongside a successful response. */
|
|
warning?: string;
|
|
stopped?: boolean;
|
|
failedNode?: string;
|
|
paused?: boolean;
|
|
taskId?: string;
|
|
runId?: string;
|
|
nodeId?: string;
|
|
}
|
|
|
|
/** Entry point / start configured outside the canvas (manual, form, schedule, …) */
|
|
export interface WorkflowEntryPoint {
|
|
id: string;
|
|
kind: string;
|
|
category: 'on_demand' | 'always_on';
|
|
enabled: boolean;
|
|
title: Record<string, string> | string;
|
|
description?: Record<string, string>;
|
|
config: Record<string, unknown>;
|
|
}
|
|
|
|
export interface WorkflowDefinition {
|
|
id: string;
|
|
label: string;
|
|
graph: WorkflowGraph;
|
|
active?: boolean;
|
|
/** Target feature instance for execution data scope (NULL for templates) */
|
|
targetFeatureInstanceId?: string | null;
|
|
/** Entry points (Starts) — how this workflow may be invoked */
|
|
invocations?: WorkflowEntryPoint[];
|
|
/** Enriched: run count */
|
|
runCount?: number;
|
|
/** Enriched: has active (running/paused) run */
|
|
isRunning?: boolean;
|
|
/** Enriched: status of active run */
|
|
runStatus?: string;
|
|
/** Enriched: nodeId where workflow is stuck (paused) */
|
|
stuckAtNodeId?: string;
|
|
/** Enriched: human-readable label for stuck node */
|
|
stuckAtNodeLabel?: string;
|
|
/** From PowerOnModel base — record creation timestamp (seconds) */
|
|
sysCreatedAt?: number;
|
|
/** Enriched: last run started timestamp (seconds) */
|
|
lastStartedAt?: number;
|
|
}
|
|
|
|
// ============================================================================
|
|
// AUTO-PREFIX TYPES (Greenfield)
|
|
// ============================================================================
|
|
|
|
export type AutoWorkflowStatus = 'draft' | 'published' | 'archived';
|
|
export type AutoRunStatus = 'running' | 'paused' | 'completed' | 'failed' | 'cancelled';
|
|
export type AutoStepStatus = 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
|
|
export type AutoTaskStatus = 'pending' | 'completed' | 'cancelled' | 'expired';
|
|
export type AutoTemplateScope = 'user' | 'instance' | 'mandate' | 'system';
|
|
|
|
export interface AutoVersion {
|
|
id: string;
|
|
workflowId: string;
|
|
versionNumber: number;
|
|
status: AutoWorkflowStatus;
|
|
graph: WorkflowGraph;
|
|
invocations?: WorkflowEntryPoint[];
|
|
publishedAt?: number;
|
|
publishedBy?: string;
|
|
}
|
|
|
|
export interface AutoRun {
|
|
id: string;
|
|
workflowId: string;
|
|
versionId?: string;
|
|
status: AutoRunStatus;
|
|
trigger?: Record<string, unknown>;
|
|
startedAt?: number;
|
|
completedAt?: number;
|
|
nodeOutputs?: Record<string, unknown>;
|
|
currentNodeId?: string;
|
|
resumeContext?: Record<string, unknown>;
|
|
error?: string;
|
|
costTokens?: number;
|
|
costCredits?: number;
|
|
}
|
|
|
|
export interface AutoWorkflow {
|
|
id: string;
|
|
mandateId: string;
|
|
featureInstanceId: string;
|
|
label: string;
|
|
description?: string;
|
|
tags?: string[];
|
|
isTemplate: boolean;
|
|
templateSourceId?: string;
|
|
templateScope?: AutoTemplateScope;
|
|
sharedReadOnly?: boolean;
|
|
currentVersionId?: string;
|
|
active: boolean;
|
|
eventId?: string;
|
|
notifyOnFailure?: boolean;
|
|
graph: WorkflowGraph;
|
|
invocations?: WorkflowEntryPoint[];
|
|
sysCreatedBy?: string;
|
|
sysCreatedAt?: number;
|
|
sysModifiedBy?: string;
|
|
sysModifiedAt?: number;
|
|
}
|
|
|
|
export interface AutoTask {
|
|
id: string;
|
|
runId: string;
|
|
workflowId: string;
|
|
nodeId: string;
|
|
nodeType: string;
|
|
config: Record<string, unknown>;
|
|
assigneeId?: string;
|
|
status: AutoTaskStatus;
|
|
result?: Record<string, unknown>;
|
|
expiresAt?: number;
|
|
sysCreatedAt?: number;
|
|
}
|
|
|
|
export interface AutoStepLog {
|
|
id: string;
|
|
runId: string;
|
|
nodeId: string;
|
|
nodeType: string;
|
|
status: AutoStepStatus;
|
|
inputSnapshot?: Record<string, unknown>;
|
|
output?: Record<string, unknown>;
|
|
error?: string;
|
|
startedAt?: number;
|
|
completedAt?: number;
|
|
durationMs?: number;
|
|
tokensUsed?: number;
|
|
retryCount?: number;
|
|
}
|
|
|
|
// ============================================================================
|
|
// ADDITIONAL TYPES
|
|
// ============================================================================
|
|
|
|
export interface UpstreamPathEntry {
|
|
producerNodeId: string;
|
|
producerLabel?: string;
|
|
path: (string | number)[];
|
|
type: string;
|
|
label: string;
|
|
scopeOrigin: 'data' | 'loop' | 'system';
|
|
valueKind?: string;
|
|
}
|
|
|
|
export interface ConditionMetaResponse {
|
|
valueKind: string;
|
|
operators: ConditionOperatorDef[];
|
|
}
|
|
|
|
export interface ConditionMetaRequest {
|
|
graph: WorkflowGraph;
|
|
nodeId?: string;
|
|
ref: { type: 'ref'; nodeId: string; path: (string | number)[] };
|
|
}
|
|
|
|
/** Scope-aware data sources for the DataPicker. */
|
|
export interface GraphDataSources {
|
|
/** Ancestor node IDs that are valid sources. */
|
|
availableSourceIds: string[];
|
|
/** Maps nodeId → output port index to use instead of 0. */
|
|
portIndexOverrides: Record<string, number>;
|
|
/** IDs of flow.loop nodes whose body the current node is inside. */
|
|
loopBodyContextIds: string[];
|
|
}
|
|
|
|
export interface ExecuteGraphOptions {
|
|
/** Use a configured start on the saved workflow */
|
|
entryPointId?: string;
|
|
/** Full run envelope (overrides entry point mapping) */
|
|
runEnvelope?: Record<string, unknown>;
|
|
/** Merged into envelope.payload */
|
|
payload?: Record<string, unknown>;
|
|
}
|
|
|
|
export interface WorkflowRun {
|
|
id: string;
|
|
workflowId: string;
|
|
status: string;
|
|
nodeOutputs?: Record<string, unknown>;
|
|
currentNodeId?: string;
|
|
}
|
|
|
|
export interface CompletedRun extends WorkflowRun {
|
|
workflowLabel?: string;
|
|
sysModifiedAt?: number;
|
|
sysCreatedAt?: number;
|
|
}
|
|
|
|
export interface WorkflowTask {
|
|
id: string;
|
|
runId: string;
|
|
workflowId: string;
|
|
nodeId: string;
|
|
nodeType: string;
|
|
config: Record<string, unknown>;
|
|
status: string;
|
|
result?: Record<string, unknown>;
|
|
/** Workflow label (enriched by API) */
|
|
workflowLabel?: string;
|
|
/** Unix timestamp ms (from sysCreatedAt) */
|
|
createdAt?: number;
|
|
/** Optional due date — configurable in future */
|
|
dueAt?: number;
|
|
}
|
|
|
|
export interface AutoWorkflowTemplate extends WorkflowDefinition {
|
|
isTemplate: boolean;
|
|
templateScope?: AutoTemplateScope;
|
|
templateSourceId?: string;
|
|
sharedReadOnly?: boolean;
|
|
}
|
|
|
|
export interface UserConnection {
|
|
id: string;
|
|
authority: string;
|
|
externalUsername?: string;
|
|
externalEmail?: string;
|
|
status: string;
|
|
}
|
|
|
|
export interface ConnectionService {
|
|
service: string;
|
|
label: string;
|
|
icon: string;
|
|
}
|
|
|
|
export interface BrowseEntry {
|
|
name: string;
|
|
path: string;
|
|
isFolder: boolean;
|
|
size?: number;
|
|
mimeType?: string;
|
|
metadata?: Record<string, unknown>;
|
|
}
|
|
|
|
export interface WorkflowMetrics {
|
|
workflowCount: number;
|
|
activeWorkflows: number;
|
|
totalRuns: number;
|
|
runsByStatus: Record<string, number>;
|
|
totalTasks: number;
|
|
tasksByStatus: Record<string, number>;
|
|
totalTokens: number;
|
|
totalCredits: number;
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Workflow file IO (envelopeVersioned, .workflow.json)
|
|
// -------------------------------------------------------------------------
|
|
|
|
export const WORKFLOW_FILE_SCHEMA_VERSION = '1.0';
|
|
export const WORKFLOW_FILE_KIND = 'poweron.workflow';
|
|
export const WORKFLOW_FILE_EXTENSION = '.workflow.json';
|
|
|
|
export interface WorkflowFileEnvelope {
|
|
$schemaVersion: string;
|
|
$kind: string;
|
|
$exportedAt?: string;
|
|
$gatewayVersion?: string;
|
|
label: string;
|
|
description?: string;
|
|
tags?: string[];
|
|
templateScope?: AutoTemplateScope;
|
|
sharedReadOnly?: boolean;
|
|
notifyOnFailure?: boolean;
|
|
graph: WorkflowGraph;
|
|
invocations?: WorkflowEntryPoint[];
|
|
}
|
|
|
|
export interface ImportWorkflowResponse {
|
|
workflow: AutoWorkflow;
|
|
warnings: string[];
|
|
created: boolean;
|
|
}
|
|
|
|
export interface ImportWorkflowOptions {
|
|
/** Inline envelope payload (preferred for round-trip in the editor). */
|
|
envelope?: WorkflowFileEnvelope;
|
|
/** UDB FileItem.id of a previously uploaded ``.workflow.json``. */
|
|
fileId?: string;
|
|
/** When set, the existing workflow is replaced instead of a new one being created. */
|
|
existingWorkflowId?: string;
|
|
}
|
|
|
|
export interface ExportWorkflowResult {
|
|
fileName: string;
|
|
envelope: WorkflowFileEnvelope;
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Workspace run types (user-facing run workspace)
|
|
// -------------------------------------------------------------------------
|
|
|
|
export interface WorkspaceRun {
|
|
id: string;
|
|
workflowId: string;
|
|
workflowLabel?: string;
|
|
status: string;
|
|
startedAt?: number;
|
|
completedAt?: number;
|
|
ownerId?: string;
|
|
mandateId?: string;
|
|
mandateLabel?: string;
|
|
targetFeatureInstanceId?: string;
|
|
targetInstanceLabel?: string;
|
|
costTokens?: number;
|
|
costCredits?: number;
|
|
error?: string;
|
|
}
|
|
|
|
export interface WorkspaceRunDetail {
|
|
run: WorkspaceRun & { nodeOutputs?: Record<string, unknown> };
|
|
workflow: {
|
|
id: string;
|
|
label: string;
|
|
targetFeatureInstanceId?: string;
|
|
featureInstanceId?: string;
|
|
tags?: string[];
|
|
} | null;
|
|
steps: Array<{
|
|
id: string;
|
|
runId: string;
|
|
nodeId: string;
|
|
nodeType: string;
|
|
status: string;
|
|
inputSnapshot?: Record<string, unknown>;
|
|
output?: Record<string, unknown>;
|
|
inputFiles?: Array<{ id: string; fileName?: string }>;
|
|
outputFiles?: Array<{ id: string; fileName?: string }>;
|
|
error?: string;
|
|
startedAt?: number;
|
|
completedAt?: number;
|
|
durationMs?: number;
|
|
tokensUsed?: number;
|
|
retryCount?: number;
|
|
}>;
|
|
files: Array<{
|
|
id: string;
|
|
fileName?: string;
|
|
contentType?: string;
|
|
sizeBytes?: number;
|
|
}>;
|
|
unassignedFiles?: Array<{
|
|
id: string;
|
|
fileName?: string;
|
|
}>;
|
|
}
|
|
|
|
// ============================================================================
|
|
// API FUNCTIONS
|
|
// ============================================================================
|
|
|
|
export type ApiRequestFunction = (options: ApiRequestOptions<any>) => Promise<any>;
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Node types & graph helpers
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Fetch node types for the flow builder (backend-driven).
|
|
* GET /api/workflow-automation/node-types?language=de
|
|
*/
|
|
export async function fetchNodeTypes(
|
|
request: ApiRequestFunction,
|
|
language = 'de'
|
|
): Promise<NodeTypesResponse> {
|
|
console.log(`${LOG} fetchNodeTypes: language=${language}`);
|
|
const data = await request({
|
|
url: `${BASE}/node-types`,
|
|
method: 'get',
|
|
params: { language },
|
|
});
|
|
const nodeTypes = data?.nodeTypes ?? [];
|
|
const categories = data?.categories ?? [];
|
|
const portTypeCatalog = data?.portTypeCatalog ?? undefined;
|
|
const conditionOperatorCatalog = data?.conditionOperatorCatalog ?? undefined;
|
|
const systemVariables = data?.systemVariables ?? undefined;
|
|
const formFieldTypes = data?.formFieldTypes ?? undefined;
|
|
console.log(
|
|
`${LOG} fetchNodeTypes response: ${nodeTypes.length} nodeTypes, ${categories.length} categories, ` +
|
|
`${portTypeCatalog ? Object.keys(portTypeCatalog).length : 0} portTypes, ` +
|
|
`${conditionOperatorCatalog ? Object.keys(conditionOperatorCatalog).length : 0} conditionKinds, ` +
|
|
`${systemVariables ? Object.keys(systemVariables).length : 0} sysVars, ` +
|
|
`${formFieldTypes ? formFieldTypes.length : 0} formFieldTypes`
|
|
);
|
|
return { nodeTypes, categories, portTypeCatalog, conditionOperatorCatalog, systemVariables, formFieldTypes };
|
|
}
|
|
|
|
/**
|
|
* POST /api/workflow-automation/condition-meta — operators for a DataRef (If/Else).
|
|
*/
|
|
export async function fetchConditionMeta(
|
|
request: ApiRequestFunction,
|
|
body: ConditionMetaRequest,
|
|
language = 'de'
|
|
): Promise<ConditionMetaResponse> {
|
|
const data = await request({
|
|
url: `${BASE}/condition-meta`,
|
|
method: 'post',
|
|
params: { language },
|
|
data: body,
|
|
});
|
|
return {
|
|
valueKind: String(data?.valueKind ?? 'unknown'),
|
|
operators: (data?.operators ?? []) as ConditionOperatorDef[],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* POST /api/workflow-automation/upstream-paths — pickable upstream paths for DataPicker / AI.
|
|
*/
|
|
export async function postUpstreamPaths(
|
|
request: ApiRequestFunction,
|
|
graph: WorkflowGraph,
|
|
nodeId: string
|
|
): Promise<{ paths: UpstreamPathEntry[] }> {
|
|
const data = await request({
|
|
url: `${BASE}/upstream-paths`,
|
|
method: 'post',
|
|
data: { graph, nodeId },
|
|
});
|
|
return { paths: (data?.paths ?? []) as UpstreamPathEntry[] };
|
|
}
|
|
|
|
/**
|
|
* POST /api/workflow-automation/graph-data-sources
|
|
*
|
|
* Returns scope-aware source list so the DataPicker needs zero graph-traversal logic.
|
|
*/
|
|
export async function fetchGraphDataSources(
|
|
request: ApiRequestFunction,
|
|
nodeId: string,
|
|
nodes: Array<{ id: string; type?: string }>,
|
|
connections: Array<{ source: string; target: string; sourceOutput?: number; targetInput?: number }>,
|
|
): Promise<GraphDataSources> {
|
|
const data = await request({
|
|
url: `${BASE}/graph-data-sources`,
|
|
method: 'post',
|
|
data: { nodeId, graph: { nodes, connections } },
|
|
});
|
|
return {
|
|
availableSourceIds: data?.availableSourceIds ?? [],
|
|
portIndexOverrides: data?.portIndexOverrides ?? {},
|
|
loopBodyContextIds: data?.loopBodyContextIds ?? [],
|
|
};
|
|
}
|
|
|
|
/** GET saved workflow graph variant of upstream-paths (requires workflowId). */
|
|
export async function getUpstreamPathsSaved(
|
|
request: ApiRequestFunction,
|
|
workflowId: string,
|
|
nodeId: string
|
|
): Promise<{ paths: UpstreamPathEntry[] }> {
|
|
const data = await request({
|
|
url: `${BASE}/upstream-paths/${encodeURIComponent(nodeId)}`,
|
|
method: 'get',
|
|
params: { workflowId },
|
|
});
|
|
return { paths: (data?.paths ?? []) as UpstreamPathEntry[] };
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Options (dropdown data for node parameters)
|
|
// -------------------------------------------------------------------------
|
|
|
|
/** GET /api/workflow-automation/options/user.connection */
|
|
export async function fetchUserConnectionOptions(
|
|
request: ApiRequestFunction,
|
|
): Promise<Array<{ value: string; label: string }>> {
|
|
const data = await request({
|
|
url: `${BASE}/options/user.connection`,
|
|
method: 'get',
|
|
});
|
|
return data?.options ?? data ?? [];
|
|
}
|
|
|
|
/** GET /api/workflow-automation/options/feature.instance */
|
|
export async function fetchFeatureInstanceOptions(
|
|
request: ApiRequestFunction,
|
|
): Promise<Array<{ value: string; label: string }>> {
|
|
const data = await request({
|
|
url: `${BASE}/options/feature.instance`,
|
|
method: 'get',
|
|
});
|
|
return data?.options ?? data ?? [];
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Execute
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Execute a workflow graph.
|
|
* POST /api/workflow-automation/workflows/{workflowId}/execute
|
|
*/
|
|
export async function executeGraph(
|
|
request: ApiRequestFunction,
|
|
graph: WorkflowGraph,
|
|
workflowId?: string,
|
|
options?: ExecuteGraphOptions
|
|
): Promise<ExecuteGraphResponse> {
|
|
console.log(
|
|
`${LOG} executeGraph request: workflowId=${workflowId} nodes=${graph.nodes.length} connections=${graph.connections.length}`,
|
|
{ nodes: graph.nodes, connections: graph.connections, options }
|
|
);
|
|
const start = performance.now();
|
|
try {
|
|
const body: Record<string, unknown> = { graph, workflowId };
|
|
if (options?.entryPointId) body.entryPointId = options.entryPointId;
|
|
if (options?.runEnvelope) body.runEnvelope = options.runEnvelope;
|
|
if (options?.payload && Object.keys(options.payload).length > 0) body.payload = options.payload;
|
|
const url = workflowId
|
|
? `${BASE}/workflows/${workflowId}/execute`
|
|
: `${BASE}/execute`;
|
|
const result = await request({ url, method: 'post', data: body });
|
|
const ms = Math.round(performance.now() - start);
|
|
console.log(
|
|
`${LOG} executeGraph response (${ms}ms): success=${result?.success} error=${result?.error ?? 'none'} nodeOutputs_keys=${Object.keys(result?.nodeOutputs ?? {}).join(',')} failedNode=${result?.failedNode ?? '-'}`,
|
|
result
|
|
);
|
|
return result;
|
|
} catch (err) {
|
|
const ms = Math.round(performance.now() - start);
|
|
console.error(`${LOG} executeGraph FAILED (${ms}ms):`, err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Workflows CRUD
|
|
// -------------------------------------------------------------------------
|
|
|
|
export async function fetchWorkflows(
|
|
request: ApiRequestFunction,
|
|
params?: { active?: boolean; pagination?: any; mandateId?: string }
|
|
): Promise<WorkflowDefinition[] | { items: WorkflowDefinition[]; pagination: any }> {
|
|
const queryParams: Record<string, any> = {};
|
|
if (params?.active !== undefined) queryParams.active = params.active;
|
|
if (params?.pagination) queryParams.pagination = JSON.stringify(params.pagination);
|
|
if (params?.mandateId) queryParams.mandateId = params.mandateId;
|
|
const data = await request({
|
|
url: `${BASE}/workflows`,
|
|
method: 'get',
|
|
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
});
|
|
if (data?.items && data?.pagination) return data;
|
|
return data?.workflows ?? [];
|
|
}
|
|
|
|
export async function fetchWorkflow(
|
|
request: ApiRequestFunction,
|
|
workflowId: string
|
|
): Promise<WorkflowDefinition> {
|
|
return await request({
|
|
url: `${BASE}/workflows/${workflowId}`,
|
|
method: 'get',
|
|
});
|
|
}
|
|
|
|
export async function createWorkflow(
|
|
request: ApiRequestFunction,
|
|
body: {
|
|
label: string;
|
|
graph: WorkflowGraph;
|
|
invocations?: WorkflowEntryPoint[];
|
|
targetFeatureInstanceId?: string | null;
|
|
mandateId?: string;
|
|
}
|
|
): Promise<WorkflowDefinition> {
|
|
return await request({
|
|
url: `${BASE}/workflows`,
|
|
method: 'post',
|
|
data: body,
|
|
});
|
|
}
|
|
|
|
export async function updateWorkflow(
|
|
request: ApiRequestFunction,
|
|
workflowId: string,
|
|
body: {
|
|
label?: string;
|
|
graph?: WorkflowGraph;
|
|
invocations?: WorkflowEntryPoint[];
|
|
active?: boolean;
|
|
notifyOnFailure?: boolean;
|
|
targetFeatureInstanceId?: string | null;
|
|
}
|
|
): Promise<WorkflowDefinition> {
|
|
return await request({
|
|
url: `${BASE}/workflows/${workflowId}`,
|
|
method: 'put',
|
|
data: body,
|
|
});
|
|
}
|
|
|
|
export async function deleteWorkflow(
|
|
request: ApiRequestFunction,
|
|
workflowId: string
|
|
): Promise<void> {
|
|
await request({
|
|
url: `${BASE}/workflows/${workflowId}`,
|
|
method: 'delete',
|
|
});
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Workflow file IO (envelopeVersioned, .workflow.json)
|
|
// -------------------------------------------------------------------------
|
|
|
|
/** POST /api/workflow-automation/workflows/import */
|
|
export async function importWorkflowFromFile(
|
|
request: ApiRequestFunction,
|
|
options: ImportWorkflowOptions,
|
|
): Promise<ImportWorkflowResponse> {
|
|
if (!options.envelope && !options.fileId) {
|
|
throw new Error('importWorkflowFromFile: either envelope or fileId is required');
|
|
}
|
|
return await request({
|
|
url: `${BASE}/workflows/import`,
|
|
method: 'post',
|
|
data: options,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* GET /api/workflow-automation/workflows/{workflowId}/export
|
|
*
|
|
* Returns ``{ fileName, envelope }`` when ``download=false`` and a raw JSON
|
|
* download (``Content-Disposition: attachment``) when ``download=true``.
|
|
*/
|
|
export async function exportWorkflowToFile(
|
|
request: ApiRequestFunction,
|
|
workflowId: string,
|
|
download = false,
|
|
): Promise<ExportWorkflowResult> {
|
|
return await request({
|
|
url: `${BASE}/workflows/${workflowId}/export`,
|
|
method: 'get',
|
|
params: { download },
|
|
});
|
|
}
|
|
|
|
/** Quick content-sniffing — used by the UDB FilesTab to flag workflow files. */
|
|
export function isWorkflowFileContent(payload: unknown): boolean {
|
|
if (!payload || typeof payload !== 'object') return false;
|
|
const p = payload as Record<string, unknown>;
|
|
return (
|
|
typeof p.$schemaVersion === 'string' &&
|
|
p.$kind === WORKFLOW_FILE_KIND &&
|
|
!!p.graph &&
|
|
typeof p.graph === 'object'
|
|
);
|
|
}
|
|
|
|
/** Suggest a safe filename from the workflow label (mirrors gateway buildFileName). */
|
|
export function workflowFileNameFor(label: string): string {
|
|
const slug = (label || 'workflow')
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9._-]+/g, '-')
|
|
.replace(/^-+|-+$/g, '')
|
|
.slice(0, 80) || 'workflow';
|
|
return `${slug}${WORKFLOW_FILE_EXTENSION}`;
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Runs
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Fetch runs for a specific workflow.
|
|
* GET /api/workflow-automation/runs?workflowId={workflowId}
|
|
*
|
|
* Replaces both:
|
|
* /api/workflows/{instanceId}/workflows/{workflowId}/runs
|
|
* /api/system/workflow-runs (with workflowId filter)
|
|
*/
|
|
export async function fetchWorkflowRuns(
|
|
request: ApiRequestFunction,
|
|
workflowId: string
|
|
): Promise<WorkflowRun[]> {
|
|
const data = await request({
|
|
url: `${BASE}/runs`,
|
|
method: 'get',
|
|
params: { workflowId },
|
|
});
|
|
return data?.runs ?? [];
|
|
}
|
|
|
|
/**
|
|
* Fetch completed runs across all workflows.
|
|
* GET /api/workflow-automation/runs?status=completed
|
|
*/
|
|
export async function fetchCompletedRuns(
|
|
request: ApiRequestFunction,
|
|
limit = 20
|
|
): Promise<CompletedRun[]> {
|
|
const data = await request({
|
|
url: `${BASE}/runs`,
|
|
method: 'get',
|
|
params: { status: 'completed', limit },
|
|
});
|
|
return data?.runs ?? [];
|
|
}
|
|
|
|
/**
|
|
* Fetch all runs (system-level / cross-workflow).
|
|
* GET /api/workflow-automation/runs
|
|
*
|
|
* Replaces /api/system/workflow-runs and /api/automations/runs.
|
|
*/
|
|
export async function fetchRuns(
|
|
request: ApiRequestFunction,
|
|
params?: {
|
|
status?: string;
|
|
workflowId?: string;
|
|
limit?: number;
|
|
offset?: number;
|
|
}
|
|
): Promise<{ runs: WorkflowRun[]; total?: number }> {
|
|
const data = await request({
|
|
url: `${BASE}/runs`,
|
|
method: 'get',
|
|
params,
|
|
});
|
|
return { runs: data?.runs ?? [], total: data?.total };
|
|
}
|
|
|
|
/** GET /api/workflow-automation/runs/{runId}/steps */
|
|
export async function fetchRunSteps(
|
|
request: ApiRequestFunction,
|
|
runId: string
|
|
): Promise<AutoStepLog[]> {
|
|
const data = await request({
|
|
url: `${BASE}/runs/${runId}/steps`,
|
|
method: 'get',
|
|
});
|
|
return data?.steps ?? [];
|
|
}
|
|
|
|
/**
|
|
* Returns the SSE stream URL for a running workflow.
|
|
* GET /api/workflow-automation/runs/{runId}/stream
|
|
*/
|
|
export async function fetchRunStream(
|
|
request: ApiRequestFunction,
|
|
runId: string
|
|
): Promise<unknown> {
|
|
return await request({
|
|
url: `${BASE}/runs/${runId}/stream`,
|
|
method: 'get',
|
|
});
|
|
}
|
|
|
|
/** POST /api/workflow-automation/runs/{runId}/stop */
|
|
export async function stopRun(
|
|
request: ApiRequestFunction,
|
|
runId: string
|
|
): Promise<{ success: boolean }> {
|
|
const data = await request({
|
|
url: `${BASE}/runs/${runId}/stop`,
|
|
method: 'post',
|
|
});
|
|
return { success: Boolean(data?.success) };
|
|
}
|
|
|
|
/** GET /api/workflow-automation/runs/{runId}/detail */
|
|
export async function fetchRunDetail(
|
|
request: ApiRequestFunction,
|
|
runId: string,
|
|
): Promise<WorkspaceRunDetail> {
|
|
const resp = await request({ url: `${BASE}/runs/${runId}/detail`, method: 'get' });
|
|
return resp as WorkspaceRunDetail;
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Workspace runs (user-facing, paginated)
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Paginated workspace runs (replaces /api/automations/runs).
|
|
* GET /api/workflow-automation/runs
|
|
*/
|
|
export async function fetchWorkspaceRuns(
|
|
request: ApiRequestFunction,
|
|
params: {
|
|
scope?: 'mine' | 'mandate';
|
|
status?: string;
|
|
targetInstanceId?: string;
|
|
workflowId?: string;
|
|
limit?: number;
|
|
offset?: number;
|
|
} = {},
|
|
): Promise<{ runs: WorkspaceRun[]; total: number }> {
|
|
const query = new URLSearchParams();
|
|
if (params.scope) query.set('scope', params.scope);
|
|
if (params.status) query.set('status', params.status);
|
|
if (params.targetInstanceId) query.set('targetInstanceId', params.targetInstanceId);
|
|
if (params.workflowId) query.set('workflowId', params.workflowId);
|
|
if (params.limit) query.set('limit', String(params.limit));
|
|
if (params.offset) query.set('offset', String(params.offset));
|
|
const qs = query.toString();
|
|
const url = `${BASE}/runs${qs ? `?${qs}` : ''}`;
|
|
const resp = await request({ url, method: 'get' });
|
|
return resp as { runs: WorkspaceRun[]; total: number };
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Tasks
|
|
// -------------------------------------------------------------------------
|
|
|
|
export async function fetchTasks(
|
|
request: ApiRequestFunction,
|
|
params?: { workflowId?: string; status?: string }
|
|
): Promise<WorkflowTask[]> {
|
|
const data = await request({
|
|
url: `${BASE}/tasks`,
|
|
method: 'get',
|
|
params,
|
|
});
|
|
return data?.tasks ?? [];
|
|
}
|
|
|
|
export async function completeTask(
|
|
request: ApiRequestFunction,
|
|
taskId: string,
|
|
result: Record<string, unknown>
|
|
): Promise<ExecuteGraphResponse> {
|
|
return await request({
|
|
url: `${BASE}/tasks/${taskId}/complete`,
|
|
method: 'post',
|
|
data: { result },
|
|
});
|
|
}
|
|
|
|
/** Cancel a pending human task and stop its workflow run. */
|
|
export async function cancelPendingTaskStopRun(
|
|
request: ApiRequestFunction,
|
|
taskId: string
|
|
): Promise<{ success: boolean; runId?: string | null; taskId: string }> {
|
|
const data = await request({
|
|
url: `${BASE}/tasks/${taskId}/cancel`,
|
|
method: 'post',
|
|
});
|
|
return {
|
|
success: Boolean(data?.success),
|
|
runId: data?.runId,
|
|
taskId: data?.taskId ?? taskId,
|
|
};
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Versions (AutoVersion Lifecycle)
|
|
// -------------------------------------------------------------------------
|
|
|
|
export async function fetchVersions(
|
|
request: ApiRequestFunction,
|
|
workflowId: string
|
|
): Promise<AutoVersion[]> {
|
|
const data = await request({
|
|
url: `${BASE}/workflows/${workflowId}/versions`,
|
|
method: 'get',
|
|
});
|
|
return data?.versions ?? [];
|
|
}
|
|
|
|
export async function createDraftVersion(
|
|
request: ApiRequestFunction,
|
|
workflowId: string
|
|
): Promise<AutoVersion> {
|
|
return await request({
|
|
url: `${BASE}/workflows/${workflowId}/versions/draft`,
|
|
method: 'post',
|
|
});
|
|
}
|
|
|
|
export async function publishVersion(
|
|
request: ApiRequestFunction,
|
|
versionId: string
|
|
): Promise<AutoVersion> {
|
|
return await request({
|
|
url: `${BASE}/versions/${versionId}/publish`,
|
|
method: 'post',
|
|
});
|
|
}
|
|
|
|
export async function unpublishVersion(
|
|
request: ApiRequestFunction,
|
|
versionId: string
|
|
): Promise<AutoVersion> {
|
|
return await request({
|
|
url: `${BASE}/versions/${versionId}/unpublish`,
|
|
method: 'post',
|
|
});
|
|
}
|
|
|
|
export async function archiveVersion(
|
|
request: ApiRequestFunction,
|
|
versionId: string
|
|
): Promise<AutoVersion> {
|
|
return await request({
|
|
url: `${BASE}/versions/${versionId}/archive`,
|
|
method: 'post',
|
|
});
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Templates
|
|
// -------------------------------------------------------------------------
|
|
|
|
export async function fetchTemplates(
|
|
request: ApiRequestFunction,
|
|
scope?: AutoTemplateScope,
|
|
pagination?: any
|
|
): Promise<AutoWorkflowTemplate[] | { items: AutoWorkflowTemplate[]; pagination: any }> {
|
|
const queryParams: Record<string, any> = {};
|
|
if (scope) queryParams.scope = scope;
|
|
if (pagination) queryParams.pagination = JSON.stringify(pagination);
|
|
const data = await request({
|
|
url: `${BASE}/templates`,
|
|
method: 'get',
|
|
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
});
|
|
if (data?.items && data?.pagination) return data;
|
|
return data?.templates ?? [];
|
|
}
|
|
|
|
export async function createTemplateFromWorkflow(
|
|
request: ApiRequestFunction,
|
|
workflowId: string,
|
|
scope: AutoTemplateScope = 'user'
|
|
): Promise<AutoWorkflowTemplate> {
|
|
return await request({
|
|
url: `${BASE}/templates/from-workflow`,
|
|
method: 'post',
|
|
data: { workflowId, scope },
|
|
});
|
|
}
|
|
|
|
export async function copyTemplate(
|
|
request: ApiRequestFunction,
|
|
templateId: string
|
|
): Promise<WorkflowDefinition> {
|
|
return await request({
|
|
url: `${BASE}/templates/${templateId}/copy`,
|
|
method: 'post',
|
|
});
|
|
}
|
|
|
|
export async function shareTemplate(
|
|
request: ApiRequestFunction,
|
|
templateId: string,
|
|
scope: AutoTemplateScope
|
|
): Promise<AutoWorkflowTemplate> {
|
|
return await request({
|
|
url: `${BASE}/templates/${templateId}/share`,
|
|
method: 'post',
|
|
data: { scope },
|
|
});
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Connections and Browse (for Email/SharePoint node config)
|
|
// -------------------------------------------------------------------------
|
|
|
|
/** Encode connection id/reference for URL path segments (may contain spaces/colons). */
|
|
function _encodedConnectionId(connectionId: string): string {
|
|
return encodeURIComponent(connectionId);
|
|
}
|
|
|
|
export async function fetchConnections(
|
|
request: ApiRequestFunction,
|
|
): Promise<UserConnection[]> {
|
|
const data = await request({
|
|
url: `${BASE}/connections`,
|
|
method: 'get',
|
|
});
|
|
return data?.connections ?? [];
|
|
}
|
|
|
|
export async function fetchConnectionServices(
|
|
request: ApiRequestFunction,
|
|
connectionId: string
|
|
): Promise<ConnectionService[]> {
|
|
const data = await request({
|
|
url: `${BASE}/connections/${_encodedConnectionId(connectionId)}/services`,
|
|
method: 'get',
|
|
});
|
|
return data?.services ?? [];
|
|
}
|
|
|
|
export async function fetchBrowse(
|
|
request: ApiRequestFunction,
|
|
connectionId: string,
|
|
service: string,
|
|
path = '/'
|
|
): Promise<{ items: BrowseEntry[]; path: string; service: string }> {
|
|
const data = await request({
|
|
url: `${BASE}/connections/${_encodedConnectionId(connectionId)}/browse`,
|
|
method: 'get',
|
|
params: { service, path },
|
|
});
|
|
return { items: data?.items ?? [], path: data?.path ?? path, service: data?.service ?? service };
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Monitoring / Metrics
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* GET /api/workflow-automation/metrics
|
|
*
|
|
* Replaces both /api/workflows/{instanceId}/metrics
|
|
* and /api/system/workflow-runs/metrics.
|
|
*/
|
|
export async function fetchMetrics(
|
|
request: ApiRequestFunction,
|
|
): Promise<WorkflowMetrics> {
|
|
return await request({
|
|
url: `${BASE}/metrics`,
|
|
method: 'get',
|
|
});
|
|
}
|