@doclo/flows 0.1.5 → 0.1.6
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.
- package/dist/index.d.ts +72 -2
- package/dist/index.js +358 -6
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -468,7 +468,7 @@ type SerializableFlow = {
|
|
|
468
468
|
/**
|
|
469
469
|
* Serializable step definition
|
|
470
470
|
*/
|
|
471
|
-
type SerializableStep = SerializableStandardStep | SerializableConditionalStep | SerializableForEachStep;
|
|
471
|
+
type SerializableStep = SerializableStandardStep | SerializableConditionalStep | SerializableForEachStep | SerializableRouteStep;
|
|
472
472
|
/**
|
|
473
473
|
* Standard sequential step
|
|
474
474
|
*/
|
|
@@ -514,6 +514,56 @@ type SerializableForEachStep = {
|
|
|
514
514
|
config: SplitConfig;
|
|
515
515
|
itemFlow: SerializableFlow | FlowReference;
|
|
516
516
|
};
|
|
517
|
+
/**
|
|
518
|
+
* Route branch configuration
|
|
519
|
+
* Maps multiple MIME types to a single branch
|
|
520
|
+
*/
|
|
521
|
+
type RouteBranchConfig = {
|
|
522
|
+
/**
|
|
523
|
+
* MIME types that route to this branch.
|
|
524
|
+
* Supports exact matches (e.g., 'application/pdf')
|
|
525
|
+
* and glob patterns (e.g., 'image/*')
|
|
526
|
+
*/
|
|
527
|
+
mimeTypes: string[];
|
|
528
|
+
/**
|
|
529
|
+
* Optional description for documentation/debugging
|
|
530
|
+
*/
|
|
531
|
+
description?: string;
|
|
532
|
+
};
|
|
533
|
+
/**
|
|
534
|
+
* Route step configuration
|
|
535
|
+
* Routes documents based on detected MIME type (no provider required)
|
|
536
|
+
*/
|
|
537
|
+
type RouteConfig = {
|
|
538
|
+
type: 'route';
|
|
539
|
+
/**
|
|
540
|
+
* Branch definitions mapping branch names to MIME type configurations.
|
|
541
|
+
* Order matters for matching - first matching branch wins.
|
|
542
|
+
*/
|
|
543
|
+
branches: Record<string, RouteBranchConfig>;
|
|
544
|
+
};
|
|
545
|
+
/**
|
|
546
|
+
* Route step (MIME-based routing + branches)
|
|
547
|
+
*
|
|
548
|
+
* Unlike conditional (which uses VLM categorization), route uses
|
|
549
|
+
* deterministic MIME detection from magic bytes - no provider needed.
|
|
550
|
+
*
|
|
551
|
+
* Branches can be either inline flows or references to separate flows.
|
|
552
|
+
* Use references to avoid hitting database JSON nesting limits.
|
|
553
|
+
*/
|
|
554
|
+
type SerializableRouteStep = {
|
|
555
|
+
type: 'route';
|
|
556
|
+
id: string;
|
|
557
|
+
name?: string;
|
|
558
|
+
nodeType: 'route';
|
|
559
|
+
config: RouteConfig;
|
|
560
|
+
branches: Record<string, SerializableFlow | FlowReference>;
|
|
561
|
+
/**
|
|
562
|
+
* Fallback branch for unmatched MIME types.
|
|
563
|
+
* If not provided and no branch matches, throws error with details.
|
|
564
|
+
*/
|
|
565
|
+
others?: SerializableFlow | FlowReference;
|
|
566
|
+
};
|
|
517
567
|
/**
|
|
518
568
|
* Input mapping configuration for trigger nodes
|
|
519
569
|
* Declarative alternatives to mapInput functions (for serialization)
|
|
@@ -781,6 +831,26 @@ interface ForEachCompositeConfig {
|
|
|
781
831
|
* Includes full observability, metrics merging, and error context.
|
|
782
832
|
*/
|
|
783
833
|
declare function createForEachCompositeNode(config: ForEachCompositeConfig): NodeDef<FlowInput, unknown[]>;
|
|
834
|
+
/**
|
|
835
|
+
* Configuration for route composite node
|
|
836
|
+
*/
|
|
837
|
+
interface RouteCompositeConfig {
|
|
838
|
+
stepId: string;
|
|
839
|
+
routeConfig: RouteConfig;
|
|
840
|
+
branches: Record<string, SerializableFlow | FlowReference>;
|
|
841
|
+
others?: SerializableFlow | FlowReference;
|
|
842
|
+
providers: ProviderRegistry;
|
|
843
|
+
flows: FlowRegistry;
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Creates a composite node that:
|
|
847
|
+
* 1. Detects MIME type from input (base64 or URL)
|
|
848
|
+
* 2. Routes to appropriate branch based on MIME type
|
|
849
|
+
* 3. Returns the branch flow's output
|
|
850
|
+
*
|
|
851
|
+
* No provider required - uses deterministic MIME detection from magic bytes.
|
|
852
|
+
*/
|
|
853
|
+
declare function createRouteCompositeNode(config: RouteCompositeConfig): NodeDef<FlowInput, unknown>;
|
|
784
854
|
|
|
785
855
|
/**
|
|
786
856
|
* Flow Validation
|
|
@@ -961,4 +1031,4 @@ declare function buildTwoProviderFlow(opts: {
|
|
|
961
1031
|
}>;
|
|
962
1032
|
};
|
|
963
1033
|
|
|
964
|
-
export { type BatchFlowResult, type BuiltFlow, type CategorizeConfig, type ConditionalCompositeConfig, type ExtractConfig, FLOW_REGISTRY, type FieldMapping, type FlowBuilder, type FlowOptions, type FlowProgressCallbacks, type FlowReference, type FlowRegistry$1 as FlowRegistry, type FlowRunOptions, FlowSerializationError, type FlowValidationResult, type ForEachCompositeConfig, type InputMappingConfig, type NodeConfig, type OutputConfig, type ParseConfig, type ProviderRegistry, type SerializableConditionalStep, type SerializableFlow, type SerializableForEachStep, type SerializableInputValidation, type SerializableStandardStep, type SerializableStep, type SplitConfig, type TriggerConfig, type ValidationError, type ValidationOptions, type ValidationResult, type ValidationWarning, buildFlowFromConfig, buildMultiProviderFlow, buildTwoProviderFlow, buildVLMDirectFlow, clearRegistry, createConditionalCompositeNode, createFlow, createForEachCompositeNode, defineFlowConfig, extractNodeMetadata, getFlow, getFlowCount, hasFlow, isBatchFlowResult, isFlowReference, isSingleFlowResult, listFlows, registerFlow, resolveFlowReference, unregisterFlow, validateFlow, validateFlowOrThrow };
|
|
1034
|
+
export { type BatchFlowResult, type BuiltFlow, type CategorizeConfig, type ConditionalCompositeConfig, type ExtractConfig, FLOW_REGISTRY, type FieldMapping, type FlowBuilder, type FlowOptions, type FlowProgressCallbacks, type FlowReference, type FlowRegistry$1 as FlowRegistry, type FlowRunOptions, FlowSerializationError, type FlowValidationResult, type ForEachCompositeConfig, type InputMappingConfig, type NodeConfig, type OutputConfig, type ParseConfig, type ProviderRegistry, type RouteBranchConfig, type RouteCompositeConfig, type RouteConfig, type SerializableConditionalStep, type SerializableFlow, type SerializableForEachStep, type SerializableInputValidation, type SerializableRouteStep, type SerializableStandardStep, type SerializableStep, type SplitConfig, type TriggerConfig, type ValidationError, type ValidationOptions, type ValidationResult, type ValidationWarning, buildFlowFromConfig, buildMultiProviderFlow, buildTwoProviderFlow, buildVLMDirectFlow, clearRegistry, createConditionalCompositeNode, createFlow, createForEachCompositeNode, createRouteCompositeNode, defineFlowConfig, extractNodeMetadata, getFlow, getFlowCount, hasFlow, isBatchFlowResult, isFlowReference, isSingleFlowResult, listFlows, registerFlow, resolveFlowReference, unregisterFlow, validateFlow, validateFlowOrThrow };
|
package/dist/index.js
CHANGED
|
@@ -53,6 +53,15 @@ function normalizeFlowInput(input) {
|
|
|
53
53
|
}
|
|
54
54
|
return input;
|
|
55
55
|
}
|
|
56
|
+
function filterInternalArtifacts(artifacts) {
|
|
57
|
+
const filtered = {};
|
|
58
|
+
for (const [key, value] of Object.entries(artifacts)) {
|
|
59
|
+
if (!key.startsWith("__")) {
|
|
60
|
+
filtered[key] = value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return filtered;
|
|
64
|
+
}
|
|
56
65
|
var Flow = class {
|
|
57
66
|
steps = [];
|
|
58
67
|
observability;
|
|
@@ -1051,7 +1060,7 @@ Correct: .conditional('step', () => parse({ provider }))`
|
|
|
1051
1060
|
results: flowResults,
|
|
1052
1061
|
metrics,
|
|
1053
1062
|
aggregated: aggregateMetrics(metrics),
|
|
1054
|
-
artifacts
|
|
1063
|
+
artifacts: filterInternalArtifacts(artifacts)
|
|
1055
1064
|
};
|
|
1056
1065
|
}
|
|
1057
1066
|
} catch (error) {
|
|
@@ -1115,7 +1124,7 @@ Correct: .conditional('step', () => parse({ provider }))`
|
|
|
1115
1124
|
outputs,
|
|
1116
1125
|
metrics,
|
|
1117
1126
|
aggregated: aggregateMetrics(metrics),
|
|
1118
|
-
artifacts
|
|
1127
|
+
artifacts: filterInternalArtifacts(artifacts)
|
|
1119
1128
|
};
|
|
1120
1129
|
} else {
|
|
1121
1130
|
result = {
|
|
@@ -1124,7 +1133,7 @@ Correct: .conditional('step', () => parse({ provider }))`
|
|
|
1124
1133
|
outputs,
|
|
1125
1134
|
metrics,
|
|
1126
1135
|
aggregated: aggregateMetrics(metrics),
|
|
1127
|
-
artifacts
|
|
1136
|
+
artifacts: filterInternalArtifacts(artifacts)
|
|
1128
1137
|
};
|
|
1129
1138
|
}
|
|
1130
1139
|
} else {
|
|
@@ -1132,7 +1141,7 @@ Correct: .conditional('step', () => parse({ provider }))`
|
|
|
1132
1141
|
output: currentData,
|
|
1133
1142
|
metrics,
|
|
1134
1143
|
aggregated: aggregateMetrics(metrics),
|
|
1135
|
-
artifacts
|
|
1144
|
+
artifacts: filterInternalArtifacts(artifacts)
|
|
1136
1145
|
};
|
|
1137
1146
|
}
|
|
1138
1147
|
if (this.observability && sampled && traceContext && executionId) {
|
|
@@ -1243,6 +1252,7 @@ import { parse, extract, split as split2, categorize as categorize2, trigger, ou
|
|
|
1243
1252
|
|
|
1244
1253
|
// src/composite-nodes.ts
|
|
1245
1254
|
import { FlowExecutionError as FlowExecutionError2 } from "@doclo/core";
|
|
1255
|
+
import { detectMimeTypeFromBase64Async, detectMimeTypeFromBase64 } from "@doclo/core";
|
|
1246
1256
|
import { categorize, split } from "@doclo/nodes";
|
|
1247
1257
|
function parseProviderName(name) {
|
|
1248
1258
|
const colonIndex = name.indexOf(":");
|
|
@@ -1622,6 +1632,214 @@ function resolveBranchFlow(flowOrRef, flows) {
|
|
|
1622
1632
|
}
|
|
1623
1633
|
return flowOrRef;
|
|
1624
1634
|
}
|
|
1635
|
+
function matchesMimePattern(mimeType, pattern) {
|
|
1636
|
+
if (pattern === mimeType) return true;
|
|
1637
|
+
if (pattern.endsWith("/*")) {
|
|
1638
|
+
const prefix = pattern.slice(0, -2);
|
|
1639
|
+
return mimeType.startsWith(prefix + "/");
|
|
1640
|
+
}
|
|
1641
|
+
return false;
|
|
1642
|
+
}
|
|
1643
|
+
function findMatchingBranch(mimeType, branches) {
|
|
1644
|
+
for (const [branchName, config] of Object.entries(branches)) {
|
|
1645
|
+
for (const pattern of config.mimeTypes) {
|
|
1646
|
+
if (matchesMimePattern(mimeType, pattern)) {
|
|
1647
|
+
return branchName;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
return void 0;
|
|
1652
|
+
}
|
|
1653
|
+
function extractBase64Data(input) {
|
|
1654
|
+
if (input.startsWith("data:")) {
|
|
1655
|
+
const commaIndex = input.indexOf(",");
|
|
1656
|
+
if (commaIndex !== -1) {
|
|
1657
|
+
return input.substring(commaIndex + 1);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
return input;
|
|
1661
|
+
}
|
|
1662
|
+
function createRouteCompositeNode(config) {
|
|
1663
|
+
const { stepId, routeConfig, branches, others, providers, flows } = config;
|
|
1664
|
+
return {
|
|
1665
|
+
key: "route-composite",
|
|
1666
|
+
run: async (input, ctx) => {
|
|
1667
|
+
const t0 = Date.now();
|
|
1668
|
+
let detectedMimeType;
|
|
1669
|
+
let selectedBranch;
|
|
1670
|
+
let phase = "detect";
|
|
1671
|
+
try {
|
|
1672
|
+
let base64Data;
|
|
1673
|
+
if (input.base64) {
|
|
1674
|
+
base64Data = extractBase64Data(input.base64);
|
|
1675
|
+
} else if (input.url) {
|
|
1676
|
+
const response = await fetch(input.url);
|
|
1677
|
+
if (!response.ok) {
|
|
1678
|
+
throw new Error(`Failed to fetch URL: ${response.status} ${response.statusText}`);
|
|
1679
|
+
}
|
|
1680
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
1681
|
+
const bytes = new Uint8Array(arrayBuffer);
|
|
1682
|
+
let binary = "";
|
|
1683
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
1684
|
+
binary += String.fromCharCode(bytes[i]);
|
|
1685
|
+
}
|
|
1686
|
+
base64Data = btoa(binary);
|
|
1687
|
+
} else {
|
|
1688
|
+
throw new Error(
|
|
1689
|
+
"Route node requires either url or base64 input for MIME detection"
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
try {
|
|
1693
|
+
detectedMimeType = await detectMimeTypeFromBase64Async(base64Data);
|
|
1694
|
+
} catch {
|
|
1695
|
+
try {
|
|
1696
|
+
detectedMimeType = detectMimeTypeFromBase64(base64Data);
|
|
1697
|
+
} catch (syncErr) {
|
|
1698
|
+
throw new Error(
|
|
1699
|
+
`Unable to detect MIME type from input. Ensure the document is a supported format. Error: ${syncErr instanceof Error ? syncErr.message : String(syncErr)}`
|
|
1700
|
+
);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
if (ctx?.emit) {
|
|
1704
|
+
ctx.emit(`${stepId}:detectedMimeType`, detectedMimeType);
|
|
1705
|
+
}
|
|
1706
|
+
phase = "route";
|
|
1707
|
+
selectedBranch = findMatchingBranch(detectedMimeType, routeConfig.branches);
|
|
1708
|
+
if (!selectedBranch && others) {
|
|
1709
|
+
selectedBranch = "others";
|
|
1710
|
+
}
|
|
1711
|
+
if (!selectedBranch) {
|
|
1712
|
+
const availableBranches = Object.entries(routeConfig.branches).map(([name, cfg]) => `${name}: [${cfg.mimeTypes.join(", ")}]`).join("; ");
|
|
1713
|
+
throw new Error(
|
|
1714
|
+
`No branch matches MIME type "${detectedMimeType}". Available branches: ${availableBranches}. Add an 'others' fallback branch to handle unmatched types.`
|
|
1715
|
+
);
|
|
1716
|
+
}
|
|
1717
|
+
if (ctx?.emit) {
|
|
1718
|
+
ctx.emit(`${stepId}:selectedBranch`, selectedBranch);
|
|
1719
|
+
}
|
|
1720
|
+
phase = "branch";
|
|
1721
|
+
const branchFlowOrRef = selectedBranch === "others" ? others : branches[selectedBranch];
|
|
1722
|
+
if (!branchFlowOrRef) {
|
|
1723
|
+
throw new Error(
|
|
1724
|
+
`Branch "${selectedBranch}" not found in route configuration`
|
|
1725
|
+
);
|
|
1726
|
+
}
|
|
1727
|
+
const branchFlowDef = resolveBranchFlow(branchFlowOrRef, flows);
|
|
1728
|
+
const branchFlow = buildFlowFromConfig(
|
|
1729
|
+
branchFlowDef,
|
|
1730
|
+
providers,
|
|
1731
|
+
flows,
|
|
1732
|
+
ctx?.observability?.config ? {
|
|
1733
|
+
observability: ctx.observability.config,
|
|
1734
|
+
metadata: {
|
|
1735
|
+
...ctx.observability?.metadata,
|
|
1736
|
+
parentNode: stepId,
|
|
1737
|
+
phase: "branch",
|
|
1738
|
+
detectedMimeType,
|
|
1739
|
+
selectedBranch
|
|
1740
|
+
}
|
|
1741
|
+
} : void 0
|
|
1742
|
+
);
|
|
1743
|
+
const branchT0 = Date.now();
|
|
1744
|
+
const branchResultRaw = await branchFlow.run(input);
|
|
1745
|
+
if (!isSingleFlowResult(branchResultRaw)) {
|
|
1746
|
+
throw new Error("Branch flow returned batch result instead of single result");
|
|
1747
|
+
}
|
|
1748
|
+
const branchResult = branchResultRaw;
|
|
1749
|
+
if (ctx?.metrics && branchResult.metrics) {
|
|
1750
|
+
const branchMetrics = flattenMetrics(
|
|
1751
|
+
branchResult.metrics,
|
|
1752
|
+
`${stepId}.branch.${selectedBranch}`
|
|
1753
|
+
);
|
|
1754
|
+
branchMetrics.forEach((m) => ctx.metrics.push(m));
|
|
1755
|
+
}
|
|
1756
|
+
if (ctx?.emit) {
|
|
1757
|
+
ctx.emit(`${stepId}:branchOutput`, branchResult.output);
|
|
1758
|
+
ctx.emit(`${stepId}:branchArtifacts`, branchResult.artifacts);
|
|
1759
|
+
}
|
|
1760
|
+
const branchCost = branchResult.metrics ? branchResult.metrics.reduce((sum, m) => sum + (m.costUSD ?? 0), 0) : 0;
|
|
1761
|
+
const totalMs = Date.now() - t0;
|
|
1762
|
+
const branchMs = branchResult.metrics ? branchResult.metrics.reduce((sum, m) => sum + (m.ms ?? 0), 0) : 0;
|
|
1763
|
+
const overheadMs = totalMs - branchMs;
|
|
1764
|
+
if (ctx?.metrics) {
|
|
1765
|
+
const wrapperMetric = {
|
|
1766
|
+
step: stepId,
|
|
1767
|
+
configStepId: ctx.stepId,
|
|
1768
|
+
startMs: t0,
|
|
1769
|
+
provider: "internal",
|
|
1770
|
+
// No external provider - uses built-in MIME detection
|
|
1771
|
+
model: "mime-detection",
|
|
1772
|
+
ms: totalMs,
|
|
1773
|
+
costUSD: branchCost,
|
|
1774
|
+
// Total cost from branch (MIME detection is free)
|
|
1775
|
+
attemptNumber: 1,
|
|
1776
|
+
// Composite wrappers don't retry, always 1
|
|
1777
|
+
metadata: {
|
|
1778
|
+
kind: "wrapper",
|
|
1779
|
+
// Distinguish wrapper from leaf metrics
|
|
1780
|
+
type: "route",
|
|
1781
|
+
rollup: true,
|
|
1782
|
+
// Duration includes child work
|
|
1783
|
+
overheadMs,
|
|
1784
|
+
// Pure wrapper overhead (MIME detection + routing)
|
|
1785
|
+
detectedMimeType,
|
|
1786
|
+
selectedBranch,
|
|
1787
|
+
branchStepCount: branchResult.metrics?.length || 0,
|
|
1788
|
+
branchFlowId: selectedBranch === "others" ? others && typeof others === "object" && "flowRef" in others ? others.flowRef : "inline" : typeof branches[selectedBranch] === "object" && "flowRef" in branches[selectedBranch] ? branches[selectedBranch].flowRef : "inline"
|
|
1789
|
+
}
|
|
1790
|
+
};
|
|
1791
|
+
ctx.metrics.push(wrapperMetric);
|
|
1792
|
+
}
|
|
1793
|
+
return branchResult.output;
|
|
1794
|
+
} catch (error) {
|
|
1795
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1796
|
+
const isNestedFlowError = err instanceof FlowExecutionError2;
|
|
1797
|
+
if (ctx?.metrics) {
|
|
1798
|
+
ctx.metrics.push({
|
|
1799
|
+
step: stepId,
|
|
1800
|
+
configStepId: ctx.stepId,
|
|
1801
|
+
startMs: t0,
|
|
1802
|
+
ms: Date.now() - t0,
|
|
1803
|
+
costUSD: 0,
|
|
1804
|
+
attemptNumber: 1,
|
|
1805
|
+
// @ts-ignore - Add error field
|
|
1806
|
+
error: err.message,
|
|
1807
|
+
metadata: {
|
|
1808
|
+
kind: "wrapper",
|
|
1809
|
+
type: "route",
|
|
1810
|
+
failed: true,
|
|
1811
|
+
detectedMimeType,
|
|
1812
|
+
selectedBranch,
|
|
1813
|
+
failedPhase: phase
|
|
1814
|
+
}
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
const flowPath = [{
|
|
1818
|
+
stepId,
|
|
1819
|
+
stepIndex: 0,
|
|
1820
|
+
stepType: "route",
|
|
1821
|
+
branch: selectedBranch || void 0
|
|
1822
|
+
}];
|
|
1823
|
+
if (isNestedFlowError && err.flowPath) {
|
|
1824
|
+
flowPath.push(...err.flowPath);
|
|
1825
|
+
}
|
|
1826
|
+
const rootCauseMessage = isNestedFlowError ? err.getRootCause().message : err.message;
|
|
1827
|
+
throw new FlowExecutionError2(
|
|
1828
|
+
`Route step "${stepId}" failed${detectedMimeType ? ` (mimeType: ${detectedMimeType})` : ""}${selectedBranch ? ` (branch: ${selectedBranch})` : ""} in phase: ${phase}
|
|
1829
|
+
Error: ${rootCauseMessage}`,
|
|
1830
|
+
stepId,
|
|
1831
|
+
0,
|
|
1832
|
+
"route",
|
|
1833
|
+
[],
|
|
1834
|
+
isNestedFlowError ? err.originalError : err,
|
|
1835
|
+
void 0,
|
|
1836
|
+
flowPath,
|
|
1837
|
+
isNestedFlowError ? err.allCompletedSteps : void 0
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1625
1843
|
|
|
1626
1844
|
// src/serialization.ts
|
|
1627
1845
|
function extractNodeMetadata(node2) {
|
|
@@ -1684,6 +1902,16 @@ function buildFlowFromConfig(flowDef, providers, flows, options) {
|
|
|
1684
1902
|
flows: flows || {}
|
|
1685
1903
|
});
|
|
1686
1904
|
flow = flow.step(step.id, node2, step.name);
|
|
1905
|
+
} else if (step.type === "route") {
|
|
1906
|
+
const node2 = createRouteCompositeNode({
|
|
1907
|
+
stepId: step.id,
|
|
1908
|
+
routeConfig: step.config,
|
|
1909
|
+
branches: step.branches,
|
|
1910
|
+
others: step.others,
|
|
1911
|
+
providers,
|
|
1912
|
+
flows: flows || {}
|
|
1913
|
+
});
|
|
1914
|
+
flow = flow.step(step.id, node2, step.name);
|
|
1687
1915
|
} else {
|
|
1688
1916
|
const exhaustiveCheck = step;
|
|
1689
1917
|
throw new FlowSerializationError(`Unknown step type: ${exhaustiveCheck.type}`);
|
|
@@ -1916,6 +2144,26 @@ function calculateFlowNestingDepth(flow, currentDepth = 1) {
|
|
|
1916
2144
|
}
|
|
1917
2145
|
}
|
|
1918
2146
|
}
|
|
2147
|
+
} else if (step.type === "route") {
|
|
2148
|
+
const routeStep = step;
|
|
2149
|
+
if (routeStep.branches) {
|
|
2150
|
+
for (const branchFlowOrRef of Object.values(routeStep.branches)) {
|
|
2151
|
+
if ("flowRef" in branchFlowOrRef) {
|
|
2152
|
+
maxDepth = Math.max(maxDepth, stepDepth + 3);
|
|
2153
|
+
} else {
|
|
2154
|
+
const branchDepth = calculateFlowNestingDepth(branchFlowOrRef, stepDepth + 2);
|
|
2155
|
+
maxDepth = Math.max(maxDepth, branchDepth);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
if (routeStep.others) {
|
|
2160
|
+
if ("flowRef" in routeStep.others) {
|
|
2161
|
+
maxDepth = Math.max(maxDepth, stepDepth + 3);
|
|
2162
|
+
} else {
|
|
2163
|
+
const othersDepth = calculateFlowNestingDepth(routeStep.others, stepDepth + 2);
|
|
2164
|
+
maxDepth = Math.max(maxDepth, othersDepth);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
1919
2167
|
} else if (step.type === "forEach") {
|
|
1920
2168
|
const forEachStep = step;
|
|
1921
2169
|
if (forEachStep.itemFlow) {
|
|
@@ -2076,8 +2324,111 @@ function validateFlow(flowDef, options = {}) {
|
|
|
2076
2324
|
}
|
|
2077
2325
|
}
|
|
2078
2326
|
}
|
|
2327
|
+
} else if (step.type === "route") {
|
|
2328
|
+
const routeStep = step;
|
|
2329
|
+
if (!routeStep.branches || typeof routeStep.branches !== "object") {
|
|
2330
|
+
errors.push({
|
|
2331
|
+
type: "invalid_config",
|
|
2332
|
+
stepId,
|
|
2333
|
+
message: "Route step missing or invalid branches field"
|
|
2334
|
+
});
|
|
2335
|
+
} else if (Object.keys(routeStep.branches).length === 0) {
|
|
2336
|
+
errors.push({
|
|
2337
|
+
type: "invalid_config",
|
|
2338
|
+
stepId,
|
|
2339
|
+
message: "Route step must have at least one branch"
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
if (routeStep.config?.branches && routeStep.branches) {
|
|
2343
|
+
for (const branchName of Object.keys(routeStep.config.branches)) {
|
|
2344
|
+
if (!routeStep.branches[branchName]) {
|
|
2345
|
+
errors.push({
|
|
2346
|
+
type: "invalid_config",
|
|
2347
|
+
stepId,
|
|
2348
|
+
message: `Branch "${branchName}" defined in config but missing flow definition`
|
|
2349
|
+
});
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
for (const [branchName, branchConfig] of Object.entries(routeStep.config.branches)) {
|
|
2353
|
+
if (!branchConfig.mimeTypes || branchConfig.mimeTypes.length === 0) {
|
|
2354
|
+
errors.push({
|
|
2355
|
+
type: "invalid_config",
|
|
2356
|
+
stepId: `${stepId}.${branchName}`,
|
|
2357
|
+
message: `Branch "${branchName}" has no MIME types defined`
|
|
2358
|
+
});
|
|
2359
|
+
} else {
|
|
2360
|
+
for (const mimeType of branchConfig.mimeTypes) {
|
|
2361
|
+
if (!mimeType.includes("/")) {
|
|
2362
|
+
errors.push({
|
|
2363
|
+
type: "invalid_config",
|
|
2364
|
+
stepId: `${stepId}.${branchName}`,
|
|
2365
|
+
message: `Invalid MIME type pattern: "${mimeType}". Expected format: "type/subtype" or "type/*"`
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
if (routeStep.branches) {
|
|
2373
|
+
for (const [branchName, branchFlowOrRef] of Object.entries(routeStep.branches)) {
|
|
2374
|
+
if ("flowRef" in branchFlowOrRef) {
|
|
2375
|
+
const flowRef = branchFlowOrRef.flowRef;
|
|
2376
|
+
if (typeof flowRef !== "string" || flowRef.trim() === "") {
|
|
2377
|
+
errors.push({
|
|
2378
|
+
type: "invalid_config",
|
|
2379
|
+
stepId: `${stepId}.${branchName}`,
|
|
2380
|
+
message: `Branch "${branchName}": flowRef must be a non-empty string`
|
|
2381
|
+
});
|
|
2382
|
+
}
|
|
2383
|
+
} else {
|
|
2384
|
+
const branchResult = validateFlow(branchFlowOrRef, options);
|
|
2385
|
+
for (const error of branchResult.errors) {
|
|
2386
|
+
errors.push({
|
|
2387
|
+
...error,
|
|
2388
|
+
stepId: `${stepId}.${branchName}`,
|
|
2389
|
+
message: `Branch "${branchName}": ${error.message}`
|
|
2390
|
+
});
|
|
2391
|
+
}
|
|
2392
|
+
for (const warning of branchResult.warnings) {
|
|
2393
|
+
warnings.push({
|
|
2394
|
+
...warning,
|
|
2395
|
+
stepId: `${stepId}.${branchName}`,
|
|
2396
|
+
message: `Branch "${branchName}": ${warning.message}`
|
|
2397
|
+
});
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
if (routeStep.others) {
|
|
2403
|
+
if ("flowRef" in routeStep.others) {
|
|
2404
|
+
const flowRef = routeStep.others.flowRef;
|
|
2405
|
+
if (typeof flowRef !== "string" || flowRef.trim() === "") {
|
|
2406
|
+
errors.push({
|
|
2407
|
+
type: "invalid_config",
|
|
2408
|
+
stepId: `${stepId}.others`,
|
|
2409
|
+
message: "Others branch flowRef must be a non-empty string"
|
|
2410
|
+
});
|
|
2411
|
+
}
|
|
2412
|
+
} else {
|
|
2413
|
+
const othersResult = validateFlow(routeStep.others, options);
|
|
2414
|
+
for (const error of othersResult.errors) {
|
|
2415
|
+
errors.push({
|
|
2416
|
+
...error,
|
|
2417
|
+
stepId: `${stepId}.others`,
|
|
2418
|
+
message: `Others branch: ${error.message}`
|
|
2419
|
+
});
|
|
2420
|
+
}
|
|
2421
|
+
for (const warning of othersResult.warnings) {
|
|
2422
|
+
warnings.push({
|
|
2423
|
+
...warning,
|
|
2424
|
+
stepId: `${stepId}.others`,
|
|
2425
|
+
message: `Others branch: ${warning.message}`
|
|
2426
|
+
});
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2079
2430
|
}
|
|
2080
|
-
const validNodeTypes = ["parse", "extract", "split", "categorize", "trigger", "output"];
|
|
2431
|
+
const validNodeTypes = ["parse", "extract", "split", "categorize", "trigger", "output", "route"];
|
|
2081
2432
|
if (!step.nodeType || !validNodeTypes.includes(step.nodeType)) {
|
|
2082
2433
|
errors.push({
|
|
2083
2434
|
type: "invalid_config",
|
|
@@ -2099,7 +2450,7 @@ function validateFlow(flowDef, options = {}) {
|
|
|
2099
2450
|
const hasProviderRef2 = (cfg) => {
|
|
2100
2451
|
return "providerRef" in cfg && typeof cfg.providerRef === "string";
|
|
2101
2452
|
};
|
|
2102
|
-
if (step.nodeType !== "trigger" && step.nodeType !== "output") {
|
|
2453
|
+
if (step.nodeType !== "trigger" && step.nodeType !== "output" && step.nodeType !== "route") {
|
|
2103
2454
|
if (!hasProviderRef2(config)) {
|
|
2104
2455
|
errors.push({
|
|
2105
2456
|
type: "missing_provider",
|
|
@@ -2609,6 +2960,7 @@ export {
|
|
|
2609
2960
|
createConditionalCompositeNode,
|
|
2610
2961
|
createFlow,
|
|
2611
2962
|
createForEachCompositeNode,
|
|
2963
|
+
createRouteCompositeNode,
|
|
2612
2964
|
defineFlowConfig,
|
|
2613
2965
|
extract2 as extract,
|
|
2614
2966
|
extractNodeMetadata,
|