@octaviaflow/flow-rules 0.4.0 → 0.6.0
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.cjs +471 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +471 -0
- package/dist/rules/configSafety.d.ts +62 -0
- package/dist/rules/flattenNestedWorkflow.d.ts +25 -0
- package/dist/rules/flowShape.d.ts +122 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -47,12 +47,15 @@ __export(exports_src, {
|
|
|
47
47
|
ConditionConfig: () => ConditionConfig,
|
|
48
48
|
DATABASE_CONNECTOR_IDS: () => DATABASE_CONNECTOR_IDS,
|
|
49
49
|
DebugConfig: () => DebugConfig,
|
|
50
|
+
EdgeType: () => EdgeType,
|
|
50
51
|
EmailConfig: () => EmailConfig,
|
|
51
52
|
FieldType: () => FieldType,
|
|
52
53
|
ForEachConfig: () => ForEachConfig,
|
|
53
54
|
JoinType: () => JoinType,
|
|
54
55
|
LogicalOperator: () => LogicalOperator,
|
|
56
|
+
MAX_STEP_CONFIG_BYTES: () => MAX_STEP_CONFIG_BYTES,
|
|
55
57
|
ManualTriggerConfig: () => ManualTriggerConfig,
|
|
58
|
+
NodeStatus: () => NodeStatus,
|
|
56
59
|
QueryMode: () => QueryMode,
|
|
57
60
|
QueryType: () => QueryType,
|
|
58
61
|
STORAGE_CONNECTOR_IDS: () => STORAGE_CONNECTOR_IDS,
|
|
@@ -64,7 +67,10 @@ __export(exports_src, {
|
|
|
64
67
|
buildPathOptions: () => buildPathOptions,
|
|
65
68
|
collectArrayPaths: () => collectArrayPaths,
|
|
66
69
|
collectArrayPathsFromSample: () => collectArrayPathsFromSample,
|
|
70
|
+
configErrors: () => configErrors,
|
|
67
71
|
emailDefinition: () => emailDefinition,
|
|
72
|
+
flattenNestedWorkflow: () => flattenNestedWorkflow,
|
|
73
|
+
generateConnectionsFromSteps: () => generateConnectionsFromSteps,
|
|
68
74
|
generateEdgeId: () => generateEdgeId,
|
|
69
75
|
generateStepId: () => generateStepId,
|
|
70
76
|
generateUUIDv4: () => generateUUIDv4,
|
|
@@ -81,8 +87,14 @@ __export(exports_src, {
|
|
|
81
87
|
migrateDelayConfig: () => migrateDelayConfig,
|
|
82
88
|
queryDefinition: () => queryDefinition,
|
|
83
89
|
storageConfigKeys: () => storageConfigKeys,
|
|
90
|
+
transformConnectionToEdge: () => transformConnectionToEdge,
|
|
91
|
+
transformFlowToWorkflow: () => transformFlowToWorkflow,
|
|
92
|
+
transformStepToNode: () => transformStepToNode,
|
|
84
93
|
validateEdge: () => validateEdge,
|
|
94
|
+
validateFlowData: () => validateFlowData,
|
|
85
95
|
validateNode: () => validateNode,
|
|
96
|
+
validateStepConfig: () => validateStepConfig,
|
|
97
|
+
validateStepConfigs: () => validateStepConfigs,
|
|
86
98
|
validateWorkflow: () => validateWorkflow
|
|
87
99
|
});
|
|
88
100
|
module.exports = __toCommonJS(exports_src);
|
|
@@ -3781,3 +3793,462 @@ function storageConfigKeys() {
|
|
|
3781
3793
|
}
|
|
3782
3794
|
return [...keys];
|
|
3783
3795
|
}
|
|
3796
|
+
// src/catalog/workflow.ts
|
|
3797
|
+
var NodeStatus;
|
|
3798
|
+
((NodeStatus) => {
|
|
3799
|
+
NodeStatus["Idle"] = "idle";
|
|
3800
|
+
NodeStatus["Running"] = "running";
|
|
3801
|
+
NodeStatus["Success"] = "success";
|
|
3802
|
+
NodeStatus["Error"] = "error";
|
|
3803
|
+
NodeStatus["Warning"] = "warning";
|
|
3804
|
+
NodeStatus["Disabled"] = "disabled";
|
|
3805
|
+
})(NodeStatus ||= {});
|
|
3806
|
+
var EdgeType;
|
|
3807
|
+
((EdgeType) => {
|
|
3808
|
+
EdgeType["Default"] = "default";
|
|
3809
|
+
EdgeType["Conditional"] = "conditional";
|
|
3810
|
+
EdgeType["Loop"] = "loop";
|
|
3811
|
+
EdgeType["Error"] = "error";
|
|
3812
|
+
})(EdgeType ||= {});
|
|
3813
|
+
// src/rules/flattenNestedWorkflow.ts
|
|
3814
|
+
function workflowIsNested(workflow) {
|
|
3815
|
+
const stack = [...workflow.steps];
|
|
3816
|
+
while (stack.length > 0) {
|
|
3817
|
+
const s = stack.pop();
|
|
3818
|
+
if (s.branches && Object.keys(s.branches).length > 0)
|
|
3819
|
+
return true;
|
|
3820
|
+
}
|
|
3821
|
+
return false;
|
|
3822
|
+
}
|
|
3823
|
+
function flattenNestedWorkflow(workflow) {
|
|
3824
|
+
if (!workflowIsNested(workflow))
|
|
3825
|
+
return workflow;
|
|
3826
|
+
const flatSteps = [];
|
|
3827
|
+
const flatConns = [...workflow.connections];
|
|
3828
|
+
const groups = [];
|
|
3829
|
+
const walk = (steps, parentIdForSubflow) => {
|
|
3830
|
+
for (const step of steps) {
|
|
3831
|
+
const { branches, ...rest } = step;
|
|
3832
|
+
const flatStep = {
|
|
3833
|
+
...rest,
|
|
3834
|
+
...parentIdForSubflow ? { parentId: parentIdForSubflow } : {}
|
|
3835
|
+
};
|
|
3836
|
+
flatSteps.push(flatStep);
|
|
3837
|
+
if (!branches)
|
|
3838
|
+
continue;
|
|
3839
|
+
if (branches.body && step.type === "group") {
|
|
3840
|
+
const body = branches.body;
|
|
3841
|
+
groups.push({
|
|
3842
|
+
group: flatStep,
|
|
3843
|
+
bodyStepIds: body.steps.map((s) => s.id),
|
|
3844
|
+
bodyConnections: body.connections
|
|
3845
|
+
});
|
|
3846
|
+
for (const c of body.connections)
|
|
3847
|
+
flatConns.push(c);
|
|
3848
|
+
walk(body.steps, step.id);
|
|
3849
|
+
}
|
|
3850
|
+
for (const branchKey of Object.keys(branches)) {
|
|
3851
|
+
if (branchKey === "body" && step.type === "group")
|
|
3852
|
+
continue;
|
|
3853
|
+
const sub = branches[branchKey];
|
|
3854
|
+
if (!sub)
|
|
3855
|
+
continue;
|
|
3856
|
+
for (const c of sub.connections)
|
|
3857
|
+
flatConns.push(c);
|
|
3858
|
+
walk(sub.steps, parentIdForSubflow);
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3861
|
+
};
|
|
3862
|
+
walk(workflow.steps, undefined);
|
|
3863
|
+
for (const g of groups) {
|
|
3864
|
+
if (g.bodyStepIds.length === 0)
|
|
3865
|
+
continue;
|
|
3866
|
+
const bodyIdSet = new Set(g.bodyStepIds);
|
|
3867
|
+
const inDeg = new Map;
|
|
3868
|
+
const outDeg = new Map;
|
|
3869
|
+
for (const id of g.bodyStepIds) {
|
|
3870
|
+
inDeg.set(id, 0);
|
|
3871
|
+
outDeg.set(id, 0);
|
|
3872
|
+
}
|
|
3873
|
+
for (const c of g.bodyConnections) {
|
|
3874
|
+
if (bodyIdSet.has(c.sourceStepId)) {
|
|
3875
|
+
outDeg.set(c.sourceStepId, (outDeg.get(c.sourceStepId) ?? 0) + 1);
|
|
3876
|
+
}
|
|
3877
|
+
if (bodyIdSet.has(c.targetStepId)) {
|
|
3878
|
+
inDeg.set(c.targetStepId, (inDeg.get(c.targetStepId) ?? 0) + 1);
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
const roots = g.bodyStepIds.filter((id) => (inDeg.get(id) ?? 0) === 0);
|
|
3882
|
+
const leaves = g.bodyStepIds.filter((id) => (outDeg.get(id) ?? 0) === 0);
|
|
3883
|
+
for (const rootId of roots) {
|
|
3884
|
+
flatConns.push({
|
|
3885
|
+
id: `synth-inner-in-${g.group.id}-${rootId}`,
|
|
3886
|
+
sourceStepId: g.group.id,
|
|
3887
|
+
targetStepId: rootId,
|
|
3888
|
+
sourcePort: "_inner_in"
|
|
3889
|
+
});
|
|
3890
|
+
}
|
|
3891
|
+
for (const leafId of leaves) {
|
|
3892
|
+
flatConns.push({
|
|
3893
|
+
id: `synth-inner-out-${leafId}-${g.group.id}`,
|
|
3894
|
+
sourceStepId: leafId,
|
|
3895
|
+
targetStepId: g.group.id,
|
|
3896
|
+
targetPort: "_inner_out"
|
|
3897
|
+
});
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
return { ...workflow, steps: flatSteps, connections: flatConns };
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3903
|
+
// src/rules/flowShape.ts
|
|
3904
|
+
function transformStepToNode(step) {
|
|
3905
|
+
const stepType = step.type;
|
|
3906
|
+
const stepActionDefId = step.actionDefinitionId ?? "";
|
|
3907
|
+
const nodeType = stepType === "trigger" ? "trigger" : stepType === "group" || stepActionDefId === "group" ? "group" : "action";
|
|
3908
|
+
let nodeStatus = "idle" /* Idle */;
|
|
3909
|
+
switch (step.status?.toLowerCase()) {
|
|
3910
|
+
case "running":
|
|
3911
|
+
case "executing":
|
|
3912
|
+
nodeStatus = "running" /* Running */;
|
|
3913
|
+
break;
|
|
3914
|
+
case "completed":
|
|
3915
|
+
case "success":
|
|
3916
|
+
nodeStatus = "success" /* Success */;
|
|
3917
|
+
break;
|
|
3918
|
+
case "failed":
|
|
3919
|
+
case "error":
|
|
3920
|
+
nodeStatus = "error" /* Error */;
|
|
3921
|
+
break;
|
|
3922
|
+
case "pending":
|
|
3923
|
+
case "waiting":
|
|
3924
|
+
nodeStatus = "idle" /* Idle */;
|
|
3925
|
+
break;
|
|
3926
|
+
default:
|
|
3927
|
+
nodeStatus = "idle" /* Idle */;
|
|
3928
|
+
}
|
|
3929
|
+
const position = step.position || { x: step.position_x || 0, y: step.position_y || 0 };
|
|
3930
|
+
const posX = typeof position === "object" && position !== null && "x" in position ? position.x : step.position_x || 0;
|
|
3931
|
+
const posY = typeof position === "object" && position !== null && "y" in position ? position.y : step.position_y || 0;
|
|
3932
|
+
const config = step.config || {};
|
|
3933
|
+
const configData = config.data || config || {};
|
|
3934
|
+
const derivedFromName = step.name ? step.name.toLowerCase().replace(/\s+/g, "_") : undefined;
|
|
3935
|
+
const resolvedActionDefinitionId = step.actionDefinitionId || config.connector_id || step.connector_id || derivedFromName || step.type;
|
|
3936
|
+
const actionDef = getActionById(resolvedActionDefinitionId);
|
|
3937
|
+
const node = {
|
|
3938
|
+
id: step.id,
|
|
3939
|
+
type: nodeType,
|
|
3940
|
+
actionDefinitionId: resolvedActionDefinitionId,
|
|
3941
|
+
actionDefinition: actionDef,
|
|
3942
|
+
label: step.label || step.name,
|
|
3943
|
+
description: step.description || config.description || actionDef?.description || "",
|
|
3944
|
+
icon: actionDef?.icon ?? getIconForStepType(step.type),
|
|
3945
|
+
position: {
|
|
3946
|
+
x: posX,
|
|
3947
|
+
y: posY
|
|
3948
|
+
},
|
|
3949
|
+
...step.parentId ? { parentId: step.parentId } : {},
|
|
3950
|
+
...step.extent ? { extent: step.extent } : {},
|
|
3951
|
+
...step.disabled ? { disabled: true } : {},
|
|
3952
|
+
...step.dimensions && nodeType === "group" ? { dimensions: step.dimensions } : nodeType === "group" ? { dimensions: { width: 480, height: 240 } } : {},
|
|
3953
|
+
config: {
|
|
3954
|
+
...configData,
|
|
3955
|
+
timeout: step.timeout_seconds,
|
|
3956
|
+
retryPolicy: step.retry_policy,
|
|
3957
|
+
notes: step.notes,
|
|
3958
|
+
tags: step.tags,
|
|
3959
|
+
stepCount: step.step_count
|
|
3960
|
+
},
|
|
3961
|
+
ports: {
|
|
3962
|
+
input: nodeType === "action" || nodeType === "group" ? [
|
|
3963
|
+
{
|
|
3964
|
+
id: `${step.id}-input`,
|
|
3965
|
+
type: "input",
|
|
3966
|
+
position: "top",
|
|
3967
|
+
connected: false
|
|
3968
|
+
}
|
|
3969
|
+
] : [],
|
|
3970
|
+
output: (() => {
|
|
3971
|
+
if (resolvedActionDefinitionId === "condition") {
|
|
3972
|
+
return [
|
|
3973
|
+
{ id: `${step.id}_output_true`, type: "output", position: "bottom", label: "True", connected: false },
|
|
3974
|
+
{ id: `${step.id}_output_false`, type: "output", position: "bottom", label: "False", connected: false }
|
|
3975
|
+
];
|
|
3976
|
+
}
|
|
3977
|
+
if (resolvedActionDefinitionId === "for_each" || resolvedActionDefinitionId === "for-each") {
|
|
3978
|
+
return [
|
|
3979
|
+
{ id: `${step.id}_output_each`, type: "output", position: "bottom", label: "Each", connected: false },
|
|
3980
|
+
{ id: `${step.id}_output_done`, type: "output", position: "bottom", label: "Done", connected: false }
|
|
3981
|
+
];
|
|
3982
|
+
}
|
|
3983
|
+
if (resolvedActionDefinitionId === "group") {
|
|
3984
|
+
return [
|
|
3985
|
+
{ id: `${step.id}_output_0`, type: "output", position: "bottom", connected: false }
|
|
3986
|
+
];
|
|
3987
|
+
}
|
|
3988
|
+
return [
|
|
3989
|
+
{ id: `${step.id}-output`, type: "output", position: "bottom", connected: false }
|
|
3990
|
+
];
|
|
3991
|
+
})()
|
|
3992
|
+
},
|
|
3993
|
+
configStatus: step.configStatus || undefined,
|
|
3994
|
+
...step.outputSchema ? { outputSchema: step.outputSchema } : {},
|
|
3995
|
+
status: nodeStatus,
|
|
3996
|
+
selected: false,
|
|
3997
|
+
dragging: false,
|
|
3998
|
+
isValid: step.configStatus ? step.configStatus === "valid" : true,
|
|
3999
|
+
expanded: step.expanded,
|
|
4000
|
+
minimized: false,
|
|
4001
|
+
locked: false,
|
|
4002
|
+
createdAt: step.created_at || new Date().toISOString(),
|
|
4003
|
+
updatedAt: step.updated_at || new Date().toISOString()
|
|
4004
|
+
};
|
|
4005
|
+
return node;
|
|
4006
|
+
}
|
|
4007
|
+
function transformConnectionToEdge(connection) {
|
|
4008
|
+
const sourceStepId = connection.sourceStepId || connection.source_step_id;
|
|
4009
|
+
const targetStepId = connection.targetStepId || connection.target_step_id;
|
|
4010
|
+
const sourcePort = connection.sourcePort || connection.source_port || `${sourceStepId}-output`;
|
|
4011
|
+
const targetPort = connection.targetPort || connection.target_port || `${targetStepId}-input`;
|
|
4012
|
+
let edgeType = "default" /* Default */;
|
|
4013
|
+
if (connection.type === "success" || connection.type === "failure") {
|
|
4014
|
+
edgeType = "conditional" /* Conditional */;
|
|
4015
|
+
}
|
|
4016
|
+
return {
|
|
4017
|
+
id: connection.id,
|
|
4018
|
+
source: sourceStepId,
|
|
4019
|
+
target: targetStepId,
|
|
4020
|
+
sourcePort,
|
|
4021
|
+
targetPort,
|
|
4022
|
+
type: edgeType,
|
|
4023
|
+
label: connection.label,
|
|
4024
|
+
selected: false,
|
|
4025
|
+
animated: false,
|
|
4026
|
+
createdAt: new Date().toISOString()
|
|
4027
|
+
};
|
|
4028
|
+
}
|
|
4029
|
+
function generateConnectionsFromSteps(steps) {
|
|
4030
|
+
const connections = [];
|
|
4031
|
+
const sortedSteps = [...steps].sort((a, b) => a.step_count - b.step_count);
|
|
4032
|
+
for (let i = 0;i < sortedSteps.length - 1; i++) {
|
|
4033
|
+
const sourceStep = sortedSteps[i];
|
|
4034
|
+
const targetStep = sortedSteps[i + 1];
|
|
4035
|
+
connections.push({
|
|
4036
|
+
id: `${sourceStep.id}-${targetStep.id}`,
|
|
4037
|
+
source_step_id: sourceStep.id,
|
|
4038
|
+
target_step_id: targetStep.id,
|
|
4039
|
+
source_port: `${sourceStep.id}-output`,
|
|
4040
|
+
target_port: `${targetStep.id}-input`,
|
|
4041
|
+
type: "default"
|
|
4042
|
+
});
|
|
4043
|
+
}
|
|
4044
|
+
return connections;
|
|
4045
|
+
}
|
|
4046
|
+
function getIconForStepType(type) {
|
|
4047
|
+
const iconMap = {
|
|
4048
|
+
trigger: "PlayOutline",
|
|
4049
|
+
transform: "Transform",
|
|
4050
|
+
action: "Application",
|
|
4051
|
+
condition: "DecisionTree",
|
|
4052
|
+
loop: "Repeat"
|
|
4053
|
+
};
|
|
4054
|
+
return iconMap[type] || "Application";
|
|
4055
|
+
}
|
|
4056
|
+
function transformFlowToWorkflow(flow) {
|
|
4057
|
+
const flatWorkflow = flattenNestedWorkflow(flow.workflow);
|
|
4058
|
+
const nodes = flatWorkflow.steps.map((step) => transformStepToNode(step));
|
|
4059
|
+
let connections = flatWorkflow.connections;
|
|
4060
|
+
if (connections === undefined || connections === null) {
|
|
4061
|
+
connections = generateConnectionsFromSteps(flatWorkflow.steps);
|
|
4062
|
+
} else {
|
|
4063
|
+
connections = connections || [];
|
|
4064
|
+
}
|
|
4065
|
+
const edges = connections.map((conn, index) => {
|
|
4066
|
+
if (!conn.id) {
|
|
4067
|
+
conn.id = `edge-${conn.source_step_id}-${conn.target_step_id}-${index}`;
|
|
4068
|
+
}
|
|
4069
|
+
return transformConnectionToEdge(conn);
|
|
4070
|
+
});
|
|
4071
|
+
edges.forEach((edge) => {
|
|
4072
|
+
const sourceNode = nodes.find((n) => n.id === edge.source);
|
|
4073
|
+
const targetNode = nodes.find((n) => n.id === edge.target);
|
|
4074
|
+
if (sourceNode) {
|
|
4075
|
+
const port = sourceNode.ports.output.find((p) => p.id === edge.sourcePort);
|
|
4076
|
+
if (port)
|
|
4077
|
+
port.connected = true;
|
|
4078
|
+
}
|
|
4079
|
+
if (targetNode) {
|
|
4080
|
+
const port = targetNode.ports.input.find((p) => p.id === edge.targetPort);
|
|
4081
|
+
if (port)
|
|
4082
|
+
port.connected = true;
|
|
4083
|
+
}
|
|
4084
|
+
});
|
|
4085
|
+
return {
|
|
4086
|
+
id: flow.workflow.id,
|
|
4087
|
+
name: flow.workflow.name,
|
|
4088
|
+
description: flow.workflow.description,
|
|
4089
|
+
nodes,
|
|
4090
|
+
edges,
|
|
4091
|
+
workflowHistory: flow.workflowHistory || []
|
|
4092
|
+
};
|
|
4093
|
+
}
|
|
4094
|
+
function validateFlowData(flow) {
|
|
4095
|
+
if (!flow || typeof flow !== "object")
|
|
4096
|
+
return false;
|
|
4097
|
+
const f = flow;
|
|
4098
|
+
return typeof f.id === "string" && typeof f.name === "string" && f.workflow !== undefined && typeof f.workflow === "object" && f.workflow !== null && Array.isArray(f.workflow.steps);
|
|
4099
|
+
}
|
|
4100
|
+
// src/rules/configSafety.ts
|
|
4101
|
+
var MAX_STEP_CONFIG_BYTES = 256 * 1024;
|
|
4102
|
+
var FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
4103
|
+
var MAX_DEPTH = 12;
|
|
4104
|
+
function typeMatches(field, value) {
|
|
4105
|
+
if (value === null || value === undefined || value === "")
|
|
4106
|
+
return true;
|
|
4107
|
+
switch (field.type) {
|
|
4108
|
+
case "number" /* Number */:
|
|
4109
|
+
return typeof value === "number" || typeof value === "string" && value.trim() !== "" && !Number.isNaN(Number(value));
|
|
4110
|
+
case "boolean" /* Boolean */:
|
|
4111
|
+
return typeof value === "boolean" || value === "true" || value === "false";
|
|
4112
|
+
case "array" /* Array */:
|
|
4113
|
+
case "multi_select" /* MultiSelect */:
|
|
4114
|
+
return Array.isArray(value);
|
|
4115
|
+
case "object" /* Object */:
|
|
4116
|
+
case "mapping" /* Mapping */:
|
|
4117
|
+
return typeof value === "object" && !Array.isArray(value);
|
|
4118
|
+
case "string" /* String */:
|
|
4119
|
+
case "code" /* Code */:
|
|
4120
|
+
case "expression" /* Expression */:
|
|
4121
|
+
case "select" /* Select */:
|
|
4122
|
+
case "connection" /* Connection */:
|
|
4123
|
+
case "secret" /* Secret */:
|
|
4124
|
+
case "url" /* URL */:
|
|
4125
|
+
case "email" /* Email */:
|
|
4126
|
+
case "phone" /* Phone */:
|
|
4127
|
+
case "color" /* Color */:
|
|
4128
|
+
case "rich_text" /* RichText */:
|
|
4129
|
+
case "markdown" /* Markdown */:
|
|
4130
|
+
case "date" /* Date */:
|
|
4131
|
+
case "datetime" /* DateTime */:
|
|
4132
|
+
return typeof value !== "object";
|
|
4133
|
+
default:
|
|
4134
|
+
return true;
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
function walk(value, stepId, path, depth, issues) {
|
|
4138
|
+
if (depth > MAX_DEPTH) {
|
|
4139
|
+
issues.push({
|
|
4140
|
+
severity: "error",
|
|
4141
|
+
code: "CONFIG_TOO_DEEP",
|
|
4142
|
+
stepId,
|
|
4143
|
+
field: path || undefined,
|
|
4144
|
+
message: `Configuration nests deeper than ${MAX_DEPTH} levels.`
|
|
4145
|
+
});
|
|
4146
|
+
return;
|
|
4147
|
+
}
|
|
4148
|
+
if (Array.isArray(value)) {
|
|
4149
|
+
for (const item of value)
|
|
4150
|
+
walk(item, stepId, path, depth + 1, issues);
|
|
4151
|
+
return;
|
|
4152
|
+
}
|
|
4153
|
+
if (value === null || typeof value !== "object")
|
|
4154
|
+
return;
|
|
4155
|
+
for (const key of Object.keys(value)) {
|
|
4156
|
+
if (FORBIDDEN_KEYS.has(key)) {
|
|
4157
|
+
issues.push({
|
|
4158
|
+
severity: "error",
|
|
4159
|
+
code: "FORBIDDEN_KEY",
|
|
4160
|
+
stepId,
|
|
4161
|
+
field: path ? `${path}.${key}` : key,
|
|
4162
|
+
message: `\`${key}\` is not allowed in a step's configuration.`
|
|
4163
|
+
});
|
|
4164
|
+
continue;
|
|
4165
|
+
}
|
|
4166
|
+
walk(value[key], stepId, path ? `${path}.${key}` : key, depth + 1, issues);
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
4169
|
+
function validateStepConfig(step) {
|
|
4170
|
+
const issues = [];
|
|
4171
|
+
const stepId = typeof step.id === "string" ? step.id : "(unknown step)";
|
|
4172
|
+
const config = step.config;
|
|
4173
|
+
if (config === null || config === undefined)
|
|
4174
|
+
return issues;
|
|
4175
|
+
if (typeof config !== "object" || Array.isArray(config)) {
|
|
4176
|
+
issues.push({
|
|
4177
|
+
severity: "error",
|
|
4178
|
+
code: "FIELD_WRONG_TYPE",
|
|
4179
|
+
stepId,
|
|
4180
|
+
message: "A step's configuration must be an object."
|
|
4181
|
+
});
|
|
4182
|
+
return issues;
|
|
4183
|
+
}
|
|
4184
|
+
let serialised;
|
|
4185
|
+
try {
|
|
4186
|
+
serialised = JSON.stringify(config);
|
|
4187
|
+
} catch {
|
|
4188
|
+
issues.push({
|
|
4189
|
+
severity: "error",
|
|
4190
|
+
code: "FIELD_WRONG_TYPE",
|
|
4191
|
+
stepId,
|
|
4192
|
+
message: "A step's configuration must be plain JSON."
|
|
4193
|
+
});
|
|
4194
|
+
return issues;
|
|
4195
|
+
}
|
|
4196
|
+
if (serialised.length > MAX_STEP_CONFIG_BYTES) {
|
|
4197
|
+
issues.push({
|
|
4198
|
+
severity: "error",
|
|
4199
|
+
code: "CONFIG_TOO_LARGE",
|
|
4200
|
+
stepId,
|
|
4201
|
+
message: `Configuration is ${Math.round(serialised.length / 1024)} KB; the limit is ${MAX_STEP_CONFIG_BYTES / 1024} KB.`
|
|
4202
|
+
});
|
|
4203
|
+
}
|
|
4204
|
+
walk(config, stepId, "", 0, issues);
|
|
4205
|
+
const definition = typeof step.actionDefinitionId === "string" ? getActionById(step.actionDefinitionId) : undefined;
|
|
4206
|
+
if (!definition)
|
|
4207
|
+
return issues;
|
|
4208
|
+
const declared = new Map(definition.inputSchema.fields.map((f) => [f.key, f]));
|
|
4209
|
+
const entries = Object.entries(config);
|
|
4210
|
+
for (const [key, value] of entries) {
|
|
4211
|
+
const field = declared.get(key);
|
|
4212
|
+
if (!field) {
|
|
4213
|
+
issues.push({
|
|
4214
|
+
severity: "warning",
|
|
4215
|
+
code: "UNDECLARED_FIELD",
|
|
4216
|
+
stepId,
|
|
4217
|
+
field: key,
|
|
4218
|
+
message: `\`${key}\` is not declared by ${definition.id}.`
|
|
4219
|
+
});
|
|
4220
|
+
continue;
|
|
4221
|
+
}
|
|
4222
|
+
if (!typeMatches(field, value)) {
|
|
4223
|
+
issues.push({
|
|
4224
|
+
severity: "error",
|
|
4225
|
+
code: "FIELD_WRONG_TYPE",
|
|
4226
|
+
stepId,
|
|
4227
|
+
field: key,
|
|
4228
|
+
message: `\`${key}\` must be ${field.type}.`
|
|
4229
|
+
});
|
|
4230
|
+
continue;
|
|
4231
|
+
}
|
|
4232
|
+
if (field.options && field.options.length > 0 && value !== null && value !== undefined && value !== "") {
|
|
4233
|
+
const allowed = new Set(field.options.map((o) => o.value));
|
|
4234
|
+
const values = Array.isArray(value) ? value : [value];
|
|
4235
|
+
const bad = values.filter((v) => (typeof v === "string" || typeof v === "number") && !allowed.has(v));
|
|
4236
|
+
if (bad.length > 0) {
|
|
4237
|
+
issues.push({
|
|
4238
|
+
severity: "warning",
|
|
4239
|
+
code: "FIELD_BAD_OPTION",
|
|
4240
|
+
stepId,
|
|
4241
|
+
field: key,
|
|
4242
|
+
message: `\`${key}\` must be one of: ${[...allowed].join(", ")}.`
|
|
4243
|
+
});
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
return issues;
|
|
4248
|
+
}
|
|
4249
|
+
function validateStepConfigs(steps) {
|
|
4250
|
+
return steps.flatMap((s) => validateStepConfig(s));
|
|
4251
|
+
}
|
|
4252
|
+
function configErrors(steps) {
|
|
4253
|
+
return validateStepConfigs(steps).filter((i) => i.severity === "error");
|
|
4254
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -40,3 +40,7 @@ export { DATABASE_CONNECTOR_IDS, type FieldMapping, TRANSFORM_OPERATIONS, Transf
|
|
|
40
40
|
export { STORAGE_CONNECTOR_IDS, type StorageExtraField, type StoragePathField, getStorageProvider, storageConfigKeys, } from "./catalog/storage/storageCapabilities";
|
|
41
41
|
export { ManualTriggerConfig } from "./catalog/triggers/manual-trigger/index";
|
|
42
42
|
export { type ManualTriggerConfigType } from "./catalog/triggers/manual-trigger/config";
|
|
43
|
+
export * from "./catalog/workflow";
|
|
44
|
+
export { type FlowConnection, type FlowData, type FlowStep, generateConnectionsFromSteps, transformConnectionToEdge, transformFlowToWorkflow, transformStepToNode, validateFlowData, } from "./rules/flowShape";
|
|
45
|
+
export { flattenNestedWorkflow } from "./rules/flattenNestedWorkflow";
|
|
46
|
+
export { configErrors, MAX_STEP_CONFIG_BYTES, validateStepConfig, validateStepConfigs, type ConfigIssue, type ConfigIssueCode, } from "./rules/configSafety";
|
package/dist/index.js
CHANGED
|
@@ -3692,6 +3692,465 @@ function storageConfigKeys() {
|
|
|
3692
3692
|
}
|
|
3693
3693
|
return [...keys];
|
|
3694
3694
|
}
|
|
3695
|
+
// src/catalog/workflow.ts
|
|
3696
|
+
var NodeStatus;
|
|
3697
|
+
((NodeStatus) => {
|
|
3698
|
+
NodeStatus["Idle"] = "idle";
|
|
3699
|
+
NodeStatus["Running"] = "running";
|
|
3700
|
+
NodeStatus["Success"] = "success";
|
|
3701
|
+
NodeStatus["Error"] = "error";
|
|
3702
|
+
NodeStatus["Warning"] = "warning";
|
|
3703
|
+
NodeStatus["Disabled"] = "disabled";
|
|
3704
|
+
})(NodeStatus ||= {});
|
|
3705
|
+
var EdgeType;
|
|
3706
|
+
((EdgeType) => {
|
|
3707
|
+
EdgeType["Default"] = "default";
|
|
3708
|
+
EdgeType["Conditional"] = "conditional";
|
|
3709
|
+
EdgeType["Loop"] = "loop";
|
|
3710
|
+
EdgeType["Error"] = "error";
|
|
3711
|
+
})(EdgeType ||= {});
|
|
3712
|
+
// src/rules/flattenNestedWorkflow.ts
|
|
3713
|
+
function workflowIsNested(workflow) {
|
|
3714
|
+
const stack = [...workflow.steps];
|
|
3715
|
+
while (stack.length > 0) {
|
|
3716
|
+
const s = stack.pop();
|
|
3717
|
+
if (s.branches && Object.keys(s.branches).length > 0)
|
|
3718
|
+
return true;
|
|
3719
|
+
}
|
|
3720
|
+
return false;
|
|
3721
|
+
}
|
|
3722
|
+
function flattenNestedWorkflow(workflow) {
|
|
3723
|
+
if (!workflowIsNested(workflow))
|
|
3724
|
+
return workflow;
|
|
3725
|
+
const flatSteps = [];
|
|
3726
|
+
const flatConns = [...workflow.connections];
|
|
3727
|
+
const groups = [];
|
|
3728
|
+
const walk = (steps, parentIdForSubflow) => {
|
|
3729
|
+
for (const step of steps) {
|
|
3730
|
+
const { branches, ...rest } = step;
|
|
3731
|
+
const flatStep = {
|
|
3732
|
+
...rest,
|
|
3733
|
+
...parentIdForSubflow ? { parentId: parentIdForSubflow } : {}
|
|
3734
|
+
};
|
|
3735
|
+
flatSteps.push(flatStep);
|
|
3736
|
+
if (!branches)
|
|
3737
|
+
continue;
|
|
3738
|
+
if (branches.body && step.type === "group") {
|
|
3739
|
+
const body = branches.body;
|
|
3740
|
+
groups.push({
|
|
3741
|
+
group: flatStep,
|
|
3742
|
+
bodyStepIds: body.steps.map((s) => s.id),
|
|
3743
|
+
bodyConnections: body.connections
|
|
3744
|
+
});
|
|
3745
|
+
for (const c of body.connections)
|
|
3746
|
+
flatConns.push(c);
|
|
3747
|
+
walk(body.steps, step.id);
|
|
3748
|
+
}
|
|
3749
|
+
for (const branchKey of Object.keys(branches)) {
|
|
3750
|
+
if (branchKey === "body" && step.type === "group")
|
|
3751
|
+
continue;
|
|
3752
|
+
const sub = branches[branchKey];
|
|
3753
|
+
if (!sub)
|
|
3754
|
+
continue;
|
|
3755
|
+
for (const c of sub.connections)
|
|
3756
|
+
flatConns.push(c);
|
|
3757
|
+
walk(sub.steps, parentIdForSubflow);
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
};
|
|
3761
|
+
walk(workflow.steps, undefined);
|
|
3762
|
+
for (const g of groups) {
|
|
3763
|
+
if (g.bodyStepIds.length === 0)
|
|
3764
|
+
continue;
|
|
3765
|
+
const bodyIdSet = new Set(g.bodyStepIds);
|
|
3766
|
+
const inDeg = new Map;
|
|
3767
|
+
const outDeg = new Map;
|
|
3768
|
+
for (const id of g.bodyStepIds) {
|
|
3769
|
+
inDeg.set(id, 0);
|
|
3770
|
+
outDeg.set(id, 0);
|
|
3771
|
+
}
|
|
3772
|
+
for (const c of g.bodyConnections) {
|
|
3773
|
+
if (bodyIdSet.has(c.sourceStepId)) {
|
|
3774
|
+
outDeg.set(c.sourceStepId, (outDeg.get(c.sourceStepId) ?? 0) + 1);
|
|
3775
|
+
}
|
|
3776
|
+
if (bodyIdSet.has(c.targetStepId)) {
|
|
3777
|
+
inDeg.set(c.targetStepId, (inDeg.get(c.targetStepId) ?? 0) + 1);
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
const roots = g.bodyStepIds.filter((id) => (inDeg.get(id) ?? 0) === 0);
|
|
3781
|
+
const leaves = g.bodyStepIds.filter((id) => (outDeg.get(id) ?? 0) === 0);
|
|
3782
|
+
for (const rootId of roots) {
|
|
3783
|
+
flatConns.push({
|
|
3784
|
+
id: `synth-inner-in-${g.group.id}-${rootId}`,
|
|
3785
|
+
sourceStepId: g.group.id,
|
|
3786
|
+
targetStepId: rootId,
|
|
3787
|
+
sourcePort: "_inner_in"
|
|
3788
|
+
});
|
|
3789
|
+
}
|
|
3790
|
+
for (const leafId of leaves) {
|
|
3791
|
+
flatConns.push({
|
|
3792
|
+
id: `synth-inner-out-${leafId}-${g.group.id}`,
|
|
3793
|
+
sourceStepId: leafId,
|
|
3794
|
+
targetStepId: g.group.id,
|
|
3795
|
+
targetPort: "_inner_out"
|
|
3796
|
+
});
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
return { ...workflow, steps: flatSteps, connections: flatConns };
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3802
|
+
// src/rules/flowShape.ts
|
|
3803
|
+
function transformStepToNode(step) {
|
|
3804
|
+
const stepType = step.type;
|
|
3805
|
+
const stepActionDefId = step.actionDefinitionId ?? "";
|
|
3806
|
+
const nodeType = stepType === "trigger" ? "trigger" : stepType === "group" || stepActionDefId === "group" ? "group" : "action";
|
|
3807
|
+
let nodeStatus = "idle" /* Idle */;
|
|
3808
|
+
switch (step.status?.toLowerCase()) {
|
|
3809
|
+
case "running":
|
|
3810
|
+
case "executing":
|
|
3811
|
+
nodeStatus = "running" /* Running */;
|
|
3812
|
+
break;
|
|
3813
|
+
case "completed":
|
|
3814
|
+
case "success":
|
|
3815
|
+
nodeStatus = "success" /* Success */;
|
|
3816
|
+
break;
|
|
3817
|
+
case "failed":
|
|
3818
|
+
case "error":
|
|
3819
|
+
nodeStatus = "error" /* Error */;
|
|
3820
|
+
break;
|
|
3821
|
+
case "pending":
|
|
3822
|
+
case "waiting":
|
|
3823
|
+
nodeStatus = "idle" /* Idle */;
|
|
3824
|
+
break;
|
|
3825
|
+
default:
|
|
3826
|
+
nodeStatus = "idle" /* Idle */;
|
|
3827
|
+
}
|
|
3828
|
+
const position = step.position || { x: step.position_x || 0, y: step.position_y || 0 };
|
|
3829
|
+
const posX = typeof position === "object" && position !== null && "x" in position ? position.x : step.position_x || 0;
|
|
3830
|
+
const posY = typeof position === "object" && position !== null && "y" in position ? position.y : step.position_y || 0;
|
|
3831
|
+
const config = step.config || {};
|
|
3832
|
+
const configData = config.data || config || {};
|
|
3833
|
+
const derivedFromName = step.name ? step.name.toLowerCase().replace(/\s+/g, "_") : undefined;
|
|
3834
|
+
const resolvedActionDefinitionId = step.actionDefinitionId || config.connector_id || step.connector_id || derivedFromName || step.type;
|
|
3835
|
+
const actionDef = getActionById(resolvedActionDefinitionId);
|
|
3836
|
+
const node = {
|
|
3837
|
+
id: step.id,
|
|
3838
|
+
type: nodeType,
|
|
3839
|
+
actionDefinitionId: resolvedActionDefinitionId,
|
|
3840
|
+
actionDefinition: actionDef,
|
|
3841
|
+
label: step.label || step.name,
|
|
3842
|
+
description: step.description || config.description || actionDef?.description || "",
|
|
3843
|
+
icon: actionDef?.icon ?? getIconForStepType(step.type),
|
|
3844
|
+
position: {
|
|
3845
|
+
x: posX,
|
|
3846
|
+
y: posY
|
|
3847
|
+
},
|
|
3848
|
+
...step.parentId ? { parentId: step.parentId } : {},
|
|
3849
|
+
...step.extent ? { extent: step.extent } : {},
|
|
3850
|
+
...step.disabled ? { disabled: true } : {},
|
|
3851
|
+
...step.dimensions && nodeType === "group" ? { dimensions: step.dimensions } : nodeType === "group" ? { dimensions: { width: 480, height: 240 } } : {},
|
|
3852
|
+
config: {
|
|
3853
|
+
...configData,
|
|
3854
|
+
timeout: step.timeout_seconds,
|
|
3855
|
+
retryPolicy: step.retry_policy,
|
|
3856
|
+
notes: step.notes,
|
|
3857
|
+
tags: step.tags,
|
|
3858
|
+
stepCount: step.step_count
|
|
3859
|
+
},
|
|
3860
|
+
ports: {
|
|
3861
|
+
input: nodeType === "action" || nodeType === "group" ? [
|
|
3862
|
+
{
|
|
3863
|
+
id: `${step.id}-input`,
|
|
3864
|
+
type: "input",
|
|
3865
|
+
position: "top",
|
|
3866
|
+
connected: false
|
|
3867
|
+
}
|
|
3868
|
+
] : [],
|
|
3869
|
+
output: (() => {
|
|
3870
|
+
if (resolvedActionDefinitionId === "condition") {
|
|
3871
|
+
return [
|
|
3872
|
+
{ id: `${step.id}_output_true`, type: "output", position: "bottom", label: "True", connected: false },
|
|
3873
|
+
{ id: `${step.id}_output_false`, type: "output", position: "bottom", label: "False", connected: false }
|
|
3874
|
+
];
|
|
3875
|
+
}
|
|
3876
|
+
if (resolvedActionDefinitionId === "for_each" || resolvedActionDefinitionId === "for-each") {
|
|
3877
|
+
return [
|
|
3878
|
+
{ id: `${step.id}_output_each`, type: "output", position: "bottom", label: "Each", connected: false },
|
|
3879
|
+
{ id: `${step.id}_output_done`, type: "output", position: "bottom", label: "Done", connected: false }
|
|
3880
|
+
];
|
|
3881
|
+
}
|
|
3882
|
+
if (resolvedActionDefinitionId === "group") {
|
|
3883
|
+
return [
|
|
3884
|
+
{ id: `${step.id}_output_0`, type: "output", position: "bottom", connected: false }
|
|
3885
|
+
];
|
|
3886
|
+
}
|
|
3887
|
+
return [
|
|
3888
|
+
{ id: `${step.id}-output`, type: "output", position: "bottom", connected: false }
|
|
3889
|
+
];
|
|
3890
|
+
})()
|
|
3891
|
+
},
|
|
3892
|
+
configStatus: step.configStatus || undefined,
|
|
3893
|
+
...step.outputSchema ? { outputSchema: step.outputSchema } : {},
|
|
3894
|
+
status: nodeStatus,
|
|
3895
|
+
selected: false,
|
|
3896
|
+
dragging: false,
|
|
3897
|
+
isValid: step.configStatus ? step.configStatus === "valid" : true,
|
|
3898
|
+
expanded: step.expanded,
|
|
3899
|
+
minimized: false,
|
|
3900
|
+
locked: false,
|
|
3901
|
+
createdAt: step.created_at || new Date().toISOString(),
|
|
3902
|
+
updatedAt: step.updated_at || new Date().toISOString()
|
|
3903
|
+
};
|
|
3904
|
+
return node;
|
|
3905
|
+
}
|
|
3906
|
+
function transformConnectionToEdge(connection) {
|
|
3907
|
+
const sourceStepId = connection.sourceStepId || connection.source_step_id;
|
|
3908
|
+
const targetStepId = connection.targetStepId || connection.target_step_id;
|
|
3909
|
+
const sourcePort = connection.sourcePort || connection.source_port || `${sourceStepId}-output`;
|
|
3910
|
+
const targetPort = connection.targetPort || connection.target_port || `${targetStepId}-input`;
|
|
3911
|
+
let edgeType = "default" /* Default */;
|
|
3912
|
+
if (connection.type === "success" || connection.type === "failure") {
|
|
3913
|
+
edgeType = "conditional" /* Conditional */;
|
|
3914
|
+
}
|
|
3915
|
+
return {
|
|
3916
|
+
id: connection.id,
|
|
3917
|
+
source: sourceStepId,
|
|
3918
|
+
target: targetStepId,
|
|
3919
|
+
sourcePort,
|
|
3920
|
+
targetPort,
|
|
3921
|
+
type: edgeType,
|
|
3922
|
+
label: connection.label,
|
|
3923
|
+
selected: false,
|
|
3924
|
+
animated: false,
|
|
3925
|
+
createdAt: new Date().toISOString()
|
|
3926
|
+
};
|
|
3927
|
+
}
|
|
3928
|
+
function generateConnectionsFromSteps(steps) {
|
|
3929
|
+
const connections = [];
|
|
3930
|
+
const sortedSteps = [...steps].sort((a, b) => a.step_count - b.step_count);
|
|
3931
|
+
for (let i = 0;i < sortedSteps.length - 1; i++) {
|
|
3932
|
+
const sourceStep = sortedSteps[i];
|
|
3933
|
+
const targetStep = sortedSteps[i + 1];
|
|
3934
|
+
connections.push({
|
|
3935
|
+
id: `${sourceStep.id}-${targetStep.id}`,
|
|
3936
|
+
source_step_id: sourceStep.id,
|
|
3937
|
+
target_step_id: targetStep.id,
|
|
3938
|
+
source_port: `${sourceStep.id}-output`,
|
|
3939
|
+
target_port: `${targetStep.id}-input`,
|
|
3940
|
+
type: "default"
|
|
3941
|
+
});
|
|
3942
|
+
}
|
|
3943
|
+
return connections;
|
|
3944
|
+
}
|
|
3945
|
+
function getIconForStepType(type) {
|
|
3946
|
+
const iconMap = {
|
|
3947
|
+
trigger: "PlayOutline",
|
|
3948
|
+
transform: "Transform",
|
|
3949
|
+
action: "Application",
|
|
3950
|
+
condition: "DecisionTree",
|
|
3951
|
+
loop: "Repeat"
|
|
3952
|
+
};
|
|
3953
|
+
return iconMap[type] || "Application";
|
|
3954
|
+
}
|
|
3955
|
+
function transformFlowToWorkflow(flow) {
|
|
3956
|
+
const flatWorkflow = flattenNestedWorkflow(flow.workflow);
|
|
3957
|
+
const nodes = flatWorkflow.steps.map((step) => transformStepToNode(step));
|
|
3958
|
+
let connections = flatWorkflow.connections;
|
|
3959
|
+
if (connections === undefined || connections === null) {
|
|
3960
|
+
connections = generateConnectionsFromSteps(flatWorkflow.steps);
|
|
3961
|
+
} else {
|
|
3962
|
+
connections = connections || [];
|
|
3963
|
+
}
|
|
3964
|
+
const edges = connections.map((conn, index) => {
|
|
3965
|
+
if (!conn.id) {
|
|
3966
|
+
conn.id = `edge-${conn.source_step_id}-${conn.target_step_id}-${index}`;
|
|
3967
|
+
}
|
|
3968
|
+
return transformConnectionToEdge(conn);
|
|
3969
|
+
});
|
|
3970
|
+
edges.forEach((edge) => {
|
|
3971
|
+
const sourceNode = nodes.find((n) => n.id === edge.source);
|
|
3972
|
+
const targetNode = nodes.find((n) => n.id === edge.target);
|
|
3973
|
+
if (sourceNode) {
|
|
3974
|
+
const port = sourceNode.ports.output.find((p) => p.id === edge.sourcePort);
|
|
3975
|
+
if (port)
|
|
3976
|
+
port.connected = true;
|
|
3977
|
+
}
|
|
3978
|
+
if (targetNode) {
|
|
3979
|
+
const port = targetNode.ports.input.find((p) => p.id === edge.targetPort);
|
|
3980
|
+
if (port)
|
|
3981
|
+
port.connected = true;
|
|
3982
|
+
}
|
|
3983
|
+
});
|
|
3984
|
+
return {
|
|
3985
|
+
id: flow.workflow.id,
|
|
3986
|
+
name: flow.workflow.name,
|
|
3987
|
+
description: flow.workflow.description,
|
|
3988
|
+
nodes,
|
|
3989
|
+
edges,
|
|
3990
|
+
workflowHistory: flow.workflowHistory || []
|
|
3991
|
+
};
|
|
3992
|
+
}
|
|
3993
|
+
function validateFlowData(flow) {
|
|
3994
|
+
if (!flow || typeof flow !== "object")
|
|
3995
|
+
return false;
|
|
3996
|
+
const f = flow;
|
|
3997
|
+
return typeof f.id === "string" && typeof f.name === "string" && f.workflow !== undefined && typeof f.workflow === "object" && f.workflow !== null && Array.isArray(f.workflow.steps);
|
|
3998
|
+
}
|
|
3999
|
+
// src/rules/configSafety.ts
|
|
4000
|
+
var MAX_STEP_CONFIG_BYTES = 256 * 1024;
|
|
4001
|
+
var FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
4002
|
+
var MAX_DEPTH = 12;
|
|
4003
|
+
function typeMatches(field, value) {
|
|
4004
|
+
if (value === null || value === undefined || value === "")
|
|
4005
|
+
return true;
|
|
4006
|
+
switch (field.type) {
|
|
4007
|
+
case "number" /* Number */:
|
|
4008
|
+
return typeof value === "number" || typeof value === "string" && value.trim() !== "" && !Number.isNaN(Number(value));
|
|
4009
|
+
case "boolean" /* Boolean */:
|
|
4010
|
+
return typeof value === "boolean" || value === "true" || value === "false";
|
|
4011
|
+
case "array" /* Array */:
|
|
4012
|
+
case "multi_select" /* MultiSelect */:
|
|
4013
|
+
return Array.isArray(value);
|
|
4014
|
+
case "object" /* Object */:
|
|
4015
|
+
case "mapping" /* Mapping */:
|
|
4016
|
+
return typeof value === "object" && !Array.isArray(value);
|
|
4017
|
+
case "string" /* String */:
|
|
4018
|
+
case "code" /* Code */:
|
|
4019
|
+
case "expression" /* Expression */:
|
|
4020
|
+
case "select" /* Select */:
|
|
4021
|
+
case "connection" /* Connection */:
|
|
4022
|
+
case "secret" /* Secret */:
|
|
4023
|
+
case "url" /* URL */:
|
|
4024
|
+
case "email" /* Email */:
|
|
4025
|
+
case "phone" /* Phone */:
|
|
4026
|
+
case "color" /* Color */:
|
|
4027
|
+
case "rich_text" /* RichText */:
|
|
4028
|
+
case "markdown" /* Markdown */:
|
|
4029
|
+
case "date" /* Date */:
|
|
4030
|
+
case "datetime" /* DateTime */:
|
|
4031
|
+
return typeof value !== "object";
|
|
4032
|
+
default:
|
|
4033
|
+
return true;
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
function walk(value, stepId, path, depth, issues) {
|
|
4037
|
+
if (depth > MAX_DEPTH) {
|
|
4038
|
+
issues.push({
|
|
4039
|
+
severity: "error",
|
|
4040
|
+
code: "CONFIG_TOO_DEEP",
|
|
4041
|
+
stepId,
|
|
4042
|
+
field: path || undefined,
|
|
4043
|
+
message: `Configuration nests deeper than ${MAX_DEPTH} levels.`
|
|
4044
|
+
});
|
|
4045
|
+
return;
|
|
4046
|
+
}
|
|
4047
|
+
if (Array.isArray(value)) {
|
|
4048
|
+
for (const item of value)
|
|
4049
|
+
walk(item, stepId, path, depth + 1, issues);
|
|
4050
|
+
return;
|
|
4051
|
+
}
|
|
4052
|
+
if (value === null || typeof value !== "object")
|
|
4053
|
+
return;
|
|
4054
|
+
for (const key of Object.keys(value)) {
|
|
4055
|
+
if (FORBIDDEN_KEYS.has(key)) {
|
|
4056
|
+
issues.push({
|
|
4057
|
+
severity: "error",
|
|
4058
|
+
code: "FORBIDDEN_KEY",
|
|
4059
|
+
stepId,
|
|
4060
|
+
field: path ? `${path}.${key}` : key,
|
|
4061
|
+
message: `\`${key}\` is not allowed in a step's configuration.`
|
|
4062
|
+
});
|
|
4063
|
+
continue;
|
|
4064
|
+
}
|
|
4065
|
+
walk(value[key], stepId, path ? `${path}.${key}` : key, depth + 1, issues);
|
|
4066
|
+
}
|
|
4067
|
+
}
|
|
4068
|
+
function validateStepConfig(step) {
|
|
4069
|
+
const issues = [];
|
|
4070
|
+
const stepId = typeof step.id === "string" ? step.id : "(unknown step)";
|
|
4071
|
+
const config = step.config;
|
|
4072
|
+
if (config === null || config === undefined)
|
|
4073
|
+
return issues;
|
|
4074
|
+
if (typeof config !== "object" || Array.isArray(config)) {
|
|
4075
|
+
issues.push({
|
|
4076
|
+
severity: "error",
|
|
4077
|
+
code: "FIELD_WRONG_TYPE",
|
|
4078
|
+
stepId,
|
|
4079
|
+
message: "A step's configuration must be an object."
|
|
4080
|
+
});
|
|
4081
|
+
return issues;
|
|
4082
|
+
}
|
|
4083
|
+
let serialised;
|
|
4084
|
+
try {
|
|
4085
|
+
serialised = JSON.stringify(config);
|
|
4086
|
+
} catch {
|
|
4087
|
+
issues.push({
|
|
4088
|
+
severity: "error",
|
|
4089
|
+
code: "FIELD_WRONG_TYPE",
|
|
4090
|
+
stepId,
|
|
4091
|
+
message: "A step's configuration must be plain JSON."
|
|
4092
|
+
});
|
|
4093
|
+
return issues;
|
|
4094
|
+
}
|
|
4095
|
+
if (serialised.length > MAX_STEP_CONFIG_BYTES) {
|
|
4096
|
+
issues.push({
|
|
4097
|
+
severity: "error",
|
|
4098
|
+
code: "CONFIG_TOO_LARGE",
|
|
4099
|
+
stepId,
|
|
4100
|
+
message: `Configuration is ${Math.round(serialised.length / 1024)} KB; the limit is ${MAX_STEP_CONFIG_BYTES / 1024} KB.`
|
|
4101
|
+
});
|
|
4102
|
+
}
|
|
4103
|
+
walk(config, stepId, "", 0, issues);
|
|
4104
|
+
const definition = typeof step.actionDefinitionId === "string" ? getActionById(step.actionDefinitionId) : undefined;
|
|
4105
|
+
if (!definition)
|
|
4106
|
+
return issues;
|
|
4107
|
+
const declared = new Map(definition.inputSchema.fields.map((f) => [f.key, f]));
|
|
4108
|
+
const entries = Object.entries(config);
|
|
4109
|
+
for (const [key, value] of entries) {
|
|
4110
|
+
const field = declared.get(key);
|
|
4111
|
+
if (!field) {
|
|
4112
|
+
issues.push({
|
|
4113
|
+
severity: "warning",
|
|
4114
|
+
code: "UNDECLARED_FIELD",
|
|
4115
|
+
stepId,
|
|
4116
|
+
field: key,
|
|
4117
|
+
message: `\`${key}\` is not declared by ${definition.id}.`
|
|
4118
|
+
});
|
|
4119
|
+
continue;
|
|
4120
|
+
}
|
|
4121
|
+
if (!typeMatches(field, value)) {
|
|
4122
|
+
issues.push({
|
|
4123
|
+
severity: "error",
|
|
4124
|
+
code: "FIELD_WRONG_TYPE",
|
|
4125
|
+
stepId,
|
|
4126
|
+
field: key,
|
|
4127
|
+
message: `\`${key}\` must be ${field.type}.`
|
|
4128
|
+
});
|
|
4129
|
+
continue;
|
|
4130
|
+
}
|
|
4131
|
+
if (field.options && field.options.length > 0 && value !== null && value !== undefined && value !== "") {
|
|
4132
|
+
const allowed = new Set(field.options.map((o) => o.value));
|
|
4133
|
+
const values = Array.isArray(value) ? value : [value];
|
|
4134
|
+
const bad = values.filter((v) => (typeof v === "string" || typeof v === "number") && !allowed.has(v));
|
|
4135
|
+
if (bad.length > 0) {
|
|
4136
|
+
issues.push({
|
|
4137
|
+
severity: "warning",
|
|
4138
|
+
code: "FIELD_BAD_OPTION",
|
|
4139
|
+
stepId,
|
|
4140
|
+
field: key,
|
|
4141
|
+
message: `\`${key}\` must be one of: ${[...allowed].join(", ")}.`
|
|
4142
|
+
});
|
|
4143
|
+
}
|
|
4144
|
+
}
|
|
4145
|
+
}
|
|
4146
|
+
return issues;
|
|
4147
|
+
}
|
|
4148
|
+
function validateStepConfigs(steps) {
|
|
4149
|
+
return steps.flatMap((s) => validateStepConfig(s));
|
|
4150
|
+
}
|
|
4151
|
+
function configErrors(steps) {
|
|
4152
|
+
return validateStepConfigs(steps).filter((i) => i.severity === "error");
|
|
4153
|
+
}
|
|
3695
4154
|
export {
|
|
3696
4155
|
ActionCategory,
|
|
3697
4156
|
ActionStatus,
|
|
@@ -3701,12 +4160,15 @@ export {
|
|
|
3701
4160
|
ConditionConfig,
|
|
3702
4161
|
DATABASE_CONNECTOR_IDS,
|
|
3703
4162
|
DebugConfig,
|
|
4163
|
+
EdgeType,
|
|
3704
4164
|
EmailConfig,
|
|
3705
4165
|
FieldType,
|
|
3706
4166
|
ForEachConfig,
|
|
3707
4167
|
JoinType,
|
|
3708
4168
|
LogicalOperator,
|
|
4169
|
+
MAX_STEP_CONFIG_BYTES,
|
|
3709
4170
|
ManualTriggerConfig,
|
|
4171
|
+
NodeStatus,
|
|
3710
4172
|
QueryMode,
|
|
3711
4173
|
QueryType,
|
|
3712
4174
|
STORAGE_CONNECTOR_IDS,
|
|
@@ -3718,7 +4180,10 @@ export {
|
|
|
3718
4180
|
buildPathOptions,
|
|
3719
4181
|
collectArrayPaths,
|
|
3720
4182
|
collectArrayPathsFromSample,
|
|
4183
|
+
configErrors,
|
|
3721
4184
|
emailDefinition,
|
|
4185
|
+
flattenNestedWorkflow,
|
|
4186
|
+
generateConnectionsFromSteps,
|
|
3722
4187
|
generateEdgeId,
|
|
3723
4188
|
generateStepId,
|
|
3724
4189
|
generateUUIDv4,
|
|
@@ -3735,7 +4200,13 @@ export {
|
|
|
3735
4200
|
migrateDelayConfig,
|
|
3736
4201
|
queryDefinition,
|
|
3737
4202
|
storageConfigKeys,
|
|
4203
|
+
transformConnectionToEdge,
|
|
4204
|
+
transformFlowToWorkflow,
|
|
4205
|
+
transformStepToNode,
|
|
3738
4206
|
validateEdge,
|
|
4207
|
+
validateFlowData,
|
|
3739
4208
|
validateNode,
|
|
4209
|
+
validateStepConfig,
|
|
4210
|
+
validateStepConfigs,
|
|
3740
4211
|
validateWorkflow
|
|
3741
4212
|
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a step's config may contain, enforced where a caller cannot skip it.
|
|
3
|
+
*
|
|
4
|
+
* The editor's rules run in a browser, so they are advice, not a fence. A
|
|
5
|
+
* request posted straight at the API — or modified on its way — reaches
|
|
6
|
+
* `PUT /flows/:id/workflow`, which validated STRUCTURE (ids, connections,
|
|
7
|
+
* cycles) and typed config as `z.record(z.string(), z.unknown())`. Any key,
|
|
8
|
+
* any value, stored verbatim.
|
|
9
|
+
*
|
|
10
|
+
* WHAT THIS DELIBERATELY DOES NOT DO: treat `inputSchema.fields` as an
|
|
11
|
+
* allow-list. Measured against the real estate, 71 of 131 distinct config
|
|
12
|
+
* keys in use are not declared there — `slack_message.text`,
|
|
13
|
+
* `query.visualConfig`, `transform.operation`, `flowId` on nearly every
|
|
14
|
+
* action. `inputSchema` describes the fields the config PANEL renders, and
|
|
15
|
+
* every action with a custom panel stores more than that. Rejecting undeclared
|
|
16
|
+
* keys would refuse most flows on the platform. So an unknown key is a
|
|
17
|
+
* WARNING, never an error.
|
|
18
|
+
*
|
|
19
|
+
* What it does enforce is the part a legitimate client never violates:
|
|
20
|
+
*
|
|
21
|
+
* 1. a declared field's declared TYPE;
|
|
22
|
+
* 2. keys that can poison an object prototype;
|
|
23
|
+
* 3. a size and depth bound, because a config is a form's worth of settings
|
|
24
|
+
* and an unbounded one is a document every reader of that flow pays for
|
|
25
|
+
* (the DEV-366 problem, in a different field).
|
|
26
|
+
*
|
|
27
|
+
* Every one of those was dry-run against all 295 steps of the 77 flows on
|
|
28
|
+
* beta before it was allowed to block anything: 0 oversized (largest 28 KB
|
|
29
|
+
* against a 256 KB cap), 0 deeper than 6 (limit 12), 0 forbidden keys, 0 type
|
|
30
|
+
* mismatches. A rule that refuses data the platform already holds is not a
|
|
31
|
+
* fence, it is an outage.
|
|
32
|
+
*
|
|
33
|
+
* An enumerated field's options are checked but only WARN, for the same
|
|
34
|
+
* reason — see the note at that check.
|
|
35
|
+
*/
|
|
36
|
+
/** Largest config a single step may carry, serialised. */
|
|
37
|
+
export declare const MAX_STEP_CONFIG_BYTES: number;
|
|
38
|
+
export type ConfigIssueCode = "FORBIDDEN_KEY" | "CONFIG_TOO_LARGE" | "CONFIG_TOO_DEEP" | "FIELD_WRONG_TYPE" | "FIELD_BAD_OPTION" | "UNDECLARED_FIELD";
|
|
39
|
+
export interface ConfigIssue {
|
|
40
|
+
severity: "error" | "warning";
|
|
41
|
+
code: ConfigIssueCode;
|
|
42
|
+
stepId: string;
|
|
43
|
+
/** The config key, when the issue is about one. */
|
|
44
|
+
field?: string;
|
|
45
|
+
message: string;
|
|
46
|
+
}
|
|
47
|
+
export interface StepLike {
|
|
48
|
+
id?: unknown;
|
|
49
|
+
actionDefinitionId?: unknown;
|
|
50
|
+
config?: unknown;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Check one step's config.
|
|
54
|
+
*
|
|
55
|
+
* Errors block a save. Warnings never do — see the file header on why an
|
|
56
|
+
* undeclared key cannot be an error.
|
|
57
|
+
*/
|
|
58
|
+
export declare function validateStepConfig(step: StepLike): ConfigIssue[];
|
|
59
|
+
/** Every step of a workflow. `steps` may be nested; flatten before calling. */
|
|
60
|
+
export declare function validateStepConfigs(steps: readonly StepLike[]): ConfigIssue[];
|
|
61
|
+
/** Just the blocking ones — what a save path refuses on. */
|
|
62
|
+
export declare function configErrors(steps: readonly StepLike[]): ConfigIssue[];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
type AnyConn = {
|
|
2
|
+
id?: string;
|
|
3
|
+
sourceStepId: string;
|
|
4
|
+
targetStepId: string;
|
|
5
|
+
sourcePort?: string;
|
|
6
|
+
targetPort?: string;
|
|
7
|
+
[k: string]: unknown;
|
|
8
|
+
};
|
|
9
|
+
type AnyStep = {
|
|
10
|
+
id: string;
|
|
11
|
+
type: string;
|
|
12
|
+
parentId?: string;
|
|
13
|
+
branches?: Record<string, {
|
|
14
|
+
steps: AnyStep[];
|
|
15
|
+
connections: AnyConn[];
|
|
16
|
+
}>;
|
|
17
|
+
[k: string]: unknown;
|
|
18
|
+
};
|
|
19
|
+
type AnyWorkflow = {
|
|
20
|
+
steps: AnyStep[];
|
|
21
|
+
connections: AnyConn[];
|
|
22
|
+
[k: string]: unknown;
|
|
23
|
+
};
|
|
24
|
+
export declare function flattenNestedWorkflow<W extends AnyWorkflow>(workflow: W): W;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The PERSISTED flow shape, and how it becomes the canvas shape.
|
|
3
|
+
*
|
|
4
|
+
* A flow is stored as `steps` + `connections`; the rules engine and anything
|
|
5
|
+
* that reasons about a flow work in `nodes` + `edges`. This is the ONE
|
|
6
|
+
* conversion between them.
|
|
7
|
+
*
|
|
8
|
+
* It lived in octaviaflow-ui because only the editor ever read a flow. The
|
|
9
|
+
* server has to now: `check_flow` runs the same rules against a STORED flow
|
|
10
|
+
* and cannot do that without converting it the same way. A second converter
|
|
11
|
+
* would make an agent's verdict quietly disagree with the editor's, which is
|
|
12
|
+
* the failure this package exists to prevent.
|
|
13
|
+
*
|
|
14
|
+
* Two UI-only pieces did not come: `loadFlowFromJSON` (an HTTP fetch) and
|
|
15
|
+
* `calculateFitViewport` (canvas geometry) stay in the editor.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Flow Loader Utilities
|
|
19
|
+
*
|
|
20
|
+
* Utilities for loading, transforming, and managing workflow flows
|
|
21
|
+
* from various data sources (JSON files, APIs, etc.)
|
|
22
|
+
*/
|
|
23
|
+
import { type WorkflowNode, type WorkflowEdge } from "../catalog/workflow";
|
|
24
|
+
/**
|
|
25
|
+
* Flow data structure from database/JSON
|
|
26
|
+
*/
|
|
27
|
+
export interface FlowData {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
description: string;
|
|
31
|
+
status: string;
|
|
32
|
+
owner: {
|
|
33
|
+
user_id: string;
|
|
34
|
+
user_name: string;
|
|
35
|
+
user_email: string;
|
|
36
|
+
permissions: string[];
|
|
37
|
+
};
|
|
38
|
+
collaborators: Array<{
|
|
39
|
+
user_id: string;
|
|
40
|
+
user_name: string;
|
|
41
|
+
user_email: string;
|
|
42
|
+
permissions: string[];
|
|
43
|
+
added_at: string;
|
|
44
|
+
added_by: string;
|
|
45
|
+
}>;
|
|
46
|
+
workflow: {
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
description: string;
|
|
50
|
+
status: string;
|
|
51
|
+
steps: FlowStep[];
|
|
52
|
+
connections?: FlowConnection[];
|
|
53
|
+
};
|
|
54
|
+
created_at?: string;
|
|
55
|
+
updated_at?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface FlowStep {
|
|
58
|
+
id: string;
|
|
59
|
+
name: string;
|
|
60
|
+
description: string;
|
|
61
|
+
type: 'trigger' | 'transform' | 'action' | 'condition' | 'loop';
|
|
62
|
+
config: {
|
|
63
|
+
connector_id?: string;
|
|
64
|
+
connection_id?: string;
|
|
65
|
+
data?: Record<string, unknown>;
|
|
66
|
+
};
|
|
67
|
+
step_count: number;
|
|
68
|
+
position_x: number;
|
|
69
|
+
position_y: number;
|
|
70
|
+
expanded: boolean;
|
|
71
|
+
timeout_seconds?: number;
|
|
72
|
+
retry_policy?: {
|
|
73
|
+
enabled: boolean;
|
|
74
|
+
max_retries: number;
|
|
75
|
+
retry_delay_seconds: number;
|
|
76
|
+
backoff_strategy: string;
|
|
77
|
+
};
|
|
78
|
+
status: string;
|
|
79
|
+
notes?: string;
|
|
80
|
+
tags?: string[];
|
|
81
|
+
created_at?: string;
|
|
82
|
+
created_by?: string;
|
|
83
|
+
updated_at?: string;
|
|
84
|
+
updated_by?: string;
|
|
85
|
+
}
|
|
86
|
+
export interface FlowConnection {
|
|
87
|
+
id: string;
|
|
88
|
+
source_step_id: string;
|
|
89
|
+
target_step_id: string;
|
|
90
|
+
source_port?: string;
|
|
91
|
+
target_port?: string;
|
|
92
|
+
condition?: string;
|
|
93
|
+
label?: string;
|
|
94
|
+
type?: 'default' | 'success' | 'failure';
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Transform flow step to WorkflowNode
|
|
98
|
+
*/
|
|
99
|
+
export declare function transformStepToNode(step: FlowStep): WorkflowNode;
|
|
100
|
+
/**
|
|
101
|
+
* Transform flow connection to WorkflowEdge
|
|
102
|
+
*/
|
|
103
|
+
export declare function transformConnectionToEdge(connection: FlowConnection): WorkflowEdge;
|
|
104
|
+
/**
|
|
105
|
+
* Auto-generate connections from step order if not provided
|
|
106
|
+
*/
|
|
107
|
+
export declare function generateConnectionsFromSteps(steps: FlowStep[]): FlowConnection[];
|
|
108
|
+
/**
|
|
109
|
+
* Transform complete flow data to workflow state
|
|
110
|
+
*/
|
|
111
|
+
export declare function transformFlowToWorkflow(flow: FlowData): {
|
|
112
|
+
id: string;
|
|
113
|
+
name: string;
|
|
114
|
+
description: string;
|
|
115
|
+
nodes: any;
|
|
116
|
+
edges: any;
|
|
117
|
+
workflowHistory: any;
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Validate flow data structure
|
|
121
|
+
*/
|
|
122
|
+
export declare function validateFlowData(flow: unknown): flow is FlowData;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octaviaflow/flow-rules",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "The flow action catalog and the rules engine behind Flow Doctor \u2014 one definition of what a step accepts and what makes a flow valid, shared by the editor and the server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|