@octaviaflow/flow-rules 0.19.1 → 0.20.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 +2044 -1987
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2044 -1987
- package/dist/rules/nodeConfigStatus.d.ts +12 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2754,859 +2754,230 @@ var getActionsByCategory = (category) => {
|
|
|
2754
2754
|
return [];
|
|
2755
2755
|
return Object.values(categoryActions);
|
|
2756
2756
|
};
|
|
2757
|
-
// src/
|
|
2758
|
-
var
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
return Array.isArray(config.blocks) && config.blocks.length > 0;
|
|
2783
|
-
}
|
|
2784
|
-
if (actionId === "query" && fieldKey === "visualConfig.fields")
|
|
2785
|
-
return queryFieldsSatisfied(config);
|
|
2786
|
-
if (actionId === "query" && fieldKey === "visualConfig.tables")
|
|
2787
|
-
return queryTablesSatisfied(config);
|
|
2788
|
-
return false;
|
|
2789
|
-
}
|
|
2790
|
-
var MESSAGE_BODY_ACTIONS = new Set([
|
|
2791
|
-
"slack_message",
|
|
2792
|
-
"teams_message",
|
|
2793
|
-
"google_chat"
|
|
2794
|
-
]);
|
|
2795
|
-
function getNestedValue(config, keyPath) {
|
|
2796
|
-
const parts = keyPath.split(".");
|
|
2797
|
-
let current = config;
|
|
2798
|
-
for (const part of parts) {
|
|
2799
|
-
if (current === null || current === undefined || typeof current !== "object") {
|
|
2800
|
-
return;
|
|
2757
|
+
// src/catalog/actions/console-log/config.ts
|
|
2758
|
+
var ConsoleLogConfig = {
|
|
2759
|
+
defaults: {
|
|
2760
|
+
message: "",
|
|
2761
|
+
level: "info",
|
|
2762
|
+
data: undefined,
|
|
2763
|
+
includeInputData: true
|
|
2764
|
+
},
|
|
2765
|
+
schema: {
|
|
2766
|
+
includeInputData: { type: "boolean", required: false },
|
|
2767
|
+
message: {
|
|
2768
|
+
type: "string",
|
|
2769
|
+
required: true,
|
|
2770
|
+
description: "Message to log"
|
|
2771
|
+
},
|
|
2772
|
+
level: {
|
|
2773
|
+
type: "select",
|
|
2774
|
+
required: false,
|
|
2775
|
+
options: ["info", "warning", "error", "debug"],
|
|
2776
|
+
description: "Log level for the message"
|
|
2777
|
+
},
|
|
2778
|
+
data: {
|
|
2779
|
+
type: "json",
|
|
2780
|
+
required: false,
|
|
2781
|
+
description: "Additional data to log"
|
|
2801
2782
|
}
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
}
|
|
2810
|
-
return field.conditional.some((cond) => {
|
|
2811
|
-
if (cond.action !== "show" && cond.action !== "require") {
|
|
2812
|
-
return false;
|
|
2783
|
+
},
|
|
2784
|
+
validate: (config) => {
|
|
2785
|
+
const errors = [];
|
|
2786
|
+
if (!config.message) {
|
|
2787
|
+
errors.push("Message is required");
|
|
2788
|
+
} else if (typeof config.message !== "string") {
|
|
2789
|
+
errors.push("Message must be a string");
|
|
2813
2790
|
}
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
case "==":
|
|
2817
|
-
return fieldValue === cond.value;
|
|
2818
|
-
case "!=":
|
|
2819
|
-
return fieldValue !== cond.value;
|
|
2820
|
-
case ">":
|
|
2821
|
-
return fieldValue > cond.value;
|
|
2822
|
-
case "<":
|
|
2823
|
-
return fieldValue < cond.value;
|
|
2824
|
-
case ">=":
|
|
2825
|
-
return fieldValue >= cond.value;
|
|
2826
|
-
case "<=":
|
|
2827
|
-
return fieldValue <= cond.value;
|
|
2828
|
-
case "in":
|
|
2829
|
-
return Array.isArray(cond.value) && cond.value.includes(fieldValue);
|
|
2830
|
-
case "not_in":
|
|
2831
|
-
return Array.isArray(cond.value) && !cond.value.includes(fieldValue);
|
|
2832
|
-
case "contains":
|
|
2833
|
-
return typeof fieldValue === "string" && fieldValue.includes(cond.value);
|
|
2834
|
-
default:
|
|
2835
|
-
return false;
|
|
2791
|
+
if (config.level && !["info", "warning", "error", "debug"].includes(config.level)) {
|
|
2792
|
+
errors.push("Invalid log level");
|
|
2836
2793
|
}
|
|
2837
|
-
});
|
|
2838
|
-
}
|
|
2839
|
-
function getNodeConfigStatus(node) {
|
|
2840
|
-
if (HIDDEN_STATUS_ACTIONS.has(node.actionDefinitionId)) {
|
|
2841
2794
|
return {
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
requiredFilled: 0,
|
|
2845
|
-
optionalTotal: 0,
|
|
2846
|
-
optionalFilled: 0,
|
|
2847
|
-
missingRequired: [],
|
|
2848
|
-
filledOptional: []
|
|
2795
|
+
valid: errors.length === 0,
|
|
2796
|
+
errors
|
|
2849
2797
|
};
|
|
2850
|
-
}
|
|
2851
|
-
|
|
2852
|
-
if (!definition) {
|
|
2798
|
+
},
|
|
2799
|
+
merge: (userConfig) => {
|
|
2853
2800
|
return {
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
requiredFilled: 0,
|
|
2857
|
-
optionalTotal: 0,
|
|
2858
|
-
optionalFilled: 0,
|
|
2859
|
-
missingRequired: [],
|
|
2860
|
-
filledOptional: []
|
|
2801
|
+
...ConsoleLogConfig.defaults,
|
|
2802
|
+
...userConfig
|
|
2861
2803
|
};
|
|
2804
|
+
},
|
|
2805
|
+
getLabel: (config) => {
|
|
2806
|
+
return `Log [${config.level}]: ${config.message}`;
|
|
2862
2807
|
}
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2808
|
+
};
|
|
2809
|
+
|
|
2810
|
+
// src/catalog/actions/delay/config.ts
|
|
2811
|
+
var DelayConfig = {
|
|
2812
|
+
defaults: {
|
|
2813
|
+
duration: 1000,
|
|
2814
|
+
unit: "milliseconds"
|
|
2815
|
+
},
|
|
2816
|
+
schema: {
|
|
2817
|
+
duration: {
|
|
2818
|
+
type: "number",
|
|
2819
|
+
required: true,
|
|
2820
|
+
description: "Duration to delay",
|
|
2821
|
+
min: 0
|
|
2822
|
+
},
|
|
2823
|
+
unit: {
|
|
2824
|
+
type: "select",
|
|
2825
|
+
required: false,
|
|
2826
|
+
options: ["milliseconds", "seconds", "minutes", "hours"],
|
|
2827
|
+
description: "Time unit for the duration"
|
|
2880
2828
|
}
|
|
2881
|
-
}
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
if (
|
|
2887
|
-
|
|
2888
|
-
|
|
2829
|
+
},
|
|
2830
|
+
validate: (config) => {
|
|
2831
|
+
const errors = [];
|
|
2832
|
+
if (config.duration === undefined || config.duration === null) {
|
|
2833
|
+
errors.push("Duration is required");
|
|
2834
|
+
} else if (typeof config.duration !== "number") {
|
|
2835
|
+
errors.push("Duration must be a number");
|
|
2836
|
+
} else if (config.duration < 0) {
|
|
2837
|
+
errors.push("Duration must be non-negative");
|
|
2889
2838
|
}
|
|
2839
|
+
if (config.unit && !["milliseconds", "seconds", "minutes", "hours"].includes(config.unit)) {
|
|
2840
|
+
errors.push("Invalid time unit");
|
|
2841
|
+
}
|
|
2842
|
+
return {
|
|
2843
|
+
valid: errors.length === 0,
|
|
2844
|
+
errors
|
|
2845
|
+
};
|
|
2846
|
+
},
|
|
2847
|
+
toMilliseconds: (duration, unit = "milliseconds") => {
|
|
2848
|
+
const multipliers = {
|
|
2849
|
+
milliseconds: 1,
|
|
2850
|
+
seconds: 1000,
|
|
2851
|
+
minutes: 60000,
|
|
2852
|
+
hours: 3600000
|
|
2853
|
+
};
|
|
2854
|
+
return duration * (multipliers[unit] || 1);
|
|
2855
|
+
},
|
|
2856
|
+
merge: (userConfig) => {
|
|
2857
|
+
return {
|
|
2858
|
+
...DelayConfig.defaults,
|
|
2859
|
+
...userConfig
|
|
2860
|
+
};
|
|
2861
|
+
},
|
|
2862
|
+
getLabel: (config) => {
|
|
2863
|
+
return `Delay ${config.duration}${config.unit ? " " + config.unit : "ms"}`;
|
|
2890
2864
|
}
|
|
2891
|
-
|
|
2892
|
-
const optionalTotal = optionalFields.length;
|
|
2893
|
-
let status;
|
|
2894
|
-
if (requiredTotal === 0) {
|
|
2895
|
-
status = "valid";
|
|
2896
|
-
} else if (missingRequired.length === 0) {
|
|
2897
|
-
status = "valid";
|
|
2898
|
-
} else if (requiredFilled > 0 || optionalFilled > 0) {
|
|
2899
|
-
status = "warning";
|
|
2900
|
-
} else {
|
|
2901
|
-
status = "error";
|
|
2902
|
-
}
|
|
2903
|
-
return {
|
|
2904
|
-
status,
|
|
2905
|
-
requiredTotal,
|
|
2906
|
-
requiredFilled,
|
|
2907
|
-
optionalTotal,
|
|
2908
|
-
optionalFilled,
|
|
2909
|
-
missingRequired,
|
|
2910
|
-
filledOptional
|
|
2911
|
-
};
|
|
2912
|
-
}
|
|
2865
|
+
};
|
|
2913
2866
|
|
|
2914
|
-
// src/
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
type: "node",
|
|
2922
|
-
severity: "error",
|
|
2923
|
-
message: "Node ID is required",
|
|
2924
|
-
nodeId: node.id
|
|
2925
|
-
});
|
|
2926
|
-
}
|
|
2927
|
-
if (!node.label || node.label.trim().length === 0) {
|
|
2928
|
-
errors.push({
|
|
2929
|
-
id: `node-${node.id}-label`,
|
|
2930
|
-
type: "node",
|
|
2931
|
-
severity: "error",
|
|
2932
|
-
message: "Node label is required",
|
|
2933
|
-
nodeId: node.id
|
|
2934
|
-
});
|
|
2935
|
-
}
|
|
2936
|
-
const cfg = getNodeConfigStatus(node);
|
|
2937
|
-
if (cfg.missingRequired.length > 0) {
|
|
2938
|
-
const fields = cfg.missingRequired.slice(0, 4).join(", ");
|
|
2939
|
-
const more = cfg.missingRequired.length > 4 ? ` +${cfg.missingRequired.length - 4} more` : "";
|
|
2940
|
-
errors.push({
|
|
2941
|
-
id: `node-${node.id}-config`,
|
|
2942
|
-
type: "node",
|
|
2943
|
-
severity: "error",
|
|
2944
|
-
message: `Missing required settings: ${fields}${more}. Open the step and fill them in.`,
|
|
2945
|
-
nodeId: node.id
|
|
2946
|
-
});
|
|
2947
|
-
}
|
|
2948
|
-
const hasOutput = allEdges.some((edge) => edge.source === node.id);
|
|
2949
|
-
if (!hasOutput && node.type === "trigger") {
|
|
2950
|
-
errors.push({
|
|
2951
|
-
id: `node-${node.id}-output`,
|
|
2952
|
-
type: "node",
|
|
2953
|
-
severity: "warning",
|
|
2954
|
-
message: `Trigger "${nodeName(node)}" has no outgoing connections`,
|
|
2955
|
-
nodeId: node.id
|
|
2956
|
-
});
|
|
2957
|
-
}
|
|
2958
|
-
return errors;
|
|
2959
|
-
}
|
|
2960
|
-
function validateEdge(edge, allNodes, allEdges = []) {
|
|
2961
|
-
const errors = [];
|
|
2962
|
-
const sourceNode = allNodes.find((n) => n.id === edge.source);
|
|
2963
|
-
if (!sourceNode) {
|
|
2964
|
-
errors.push({
|
|
2965
|
-
id: `edge-${edge.id}-source`,
|
|
2966
|
-
type: "edge",
|
|
2967
|
-
severity: "error",
|
|
2968
|
-
message: `Source node "${edge.source}" does not exist`,
|
|
2969
|
-
edgeId: edge.id
|
|
2970
|
-
});
|
|
2971
|
-
}
|
|
2972
|
-
const targetNode = allNodes.find((n) => n.id === edge.target);
|
|
2973
|
-
if (!targetNode) {
|
|
2974
|
-
errors.push({
|
|
2975
|
-
id: `edge-${edge.id}-target`,
|
|
2976
|
-
type: "edge",
|
|
2977
|
-
severity: "error",
|
|
2978
|
-
message: `Target node "${edge.target}" does not exist`,
|
|
2979
|
-
edgeId: edge.id
|
|
2980
|
-
});
|
|
2981
|
-
}
|
|
2982
|
-
if (edge.source === edge.target) {
|
|
2983
|
-
errors.push({
|
|
2984
|
-
id: `edge-${edge.id}-loop`,
|
|
2985
|
-
type: "edge",
|
|
2986
|
-
severity: "error",
|
|
2987
|
-
message: "Nodes cannot connect to themselves",
|
|
2988
|
-
edgeId: edge.id
|
|
2989
|
-
});
|
|
2990
|
-
}
|
|
2991
|
-
const key = (e) => `${e.source}|${e.sourceHandle ?? ""}→${e.target}|${e.targetHandle ?? ""}`;
|
|
2992
|
-
const myKey = key(edge);
|
|
2993
|
-
const firstIndex = allEdges.findIndex((e) => key(e) === myKey);
|
|
2994
|
-
const myIndex = allEdges.findIndex((e) => e.id === edge.id);
|
|
2995
|
-
if (firstIndex !== -1 && myIndex > firstIndex) {
|
|
2996
|
-
errors.push({
|
|
2997
|
-
id: `edge-${edge.id}-duplicate`,
|
|
2998
|
-
type: "edge",
|
|
2999
|
-
severity: "warning",
|
|
3000
|
-
message: `Duplicate connection between "${sourceNode ? nodeName(sourceNode) : edge.source}" and "${targetNode ? nodeName(targetNode) : edge.target}"`,
|
|
3001
|
-
edgeId: edge.id
|
|
3002
|
-
});
|
|
3003
|
-
}
|
|
3004
|
-
return errors;
|
|
3005
|
-
}
|
|
3006
|
-
function validateWorkflow(nodes, edges) {
|
|
3007
|
-
const allErrors = [];
|
|
3008
|
-
const hasTrigger = nodes.some((node) => node.type === "trigger");
|
|
3009
|
-
if (!hasTrigger) {
|
|
3010
|
-
allErrors.push({
|
|
3011
|
-
id: "workflow-trigger",
|
|
3012
|
-
type: "workflow",
|
|
3013
|
-
severity: "error",
|
|
3014
|
-
message: "Workflow must have at least one trigger node"
|
|
3015
|
-
});
|
|
3016
|
-
}
|
|
3017
|
-
const hasAction = nodes.some((node) => node.type === "action");
|
|
3018
|
-
if (!hasAction && nodes.length > 0) {
|
|
3019
|
-
allErrors.push({
|
|
3020
|
-
id: "workflow-action",
|
|
3021
|
-
type: "workflow",
|
|
3022
|
-
severity: "warning",
|
|
3023
|
-
message: "Workflow should have at least one action node"
|
|
3024
|
-
});
|
|
3025
|
-
}
|
|
3026
|
-
nodes.forEach((node) => {
|
|
3027
|
-
const nodeErrors = validateNode(node, nodes, edges);
|
|
3028
|
-
allErrors.push(...nodeErrors);
|
|
3029
|
-
});
|
|
3030
|
-
edges.forEach((edge) => {
|
|
3031
|
-
const edgeErrors = validateEdge(edge, nodes, edges);
|
|
3032
|
-
allErrors.push(...edgeErrors);
|
|
3033
|
-
});
|
|
3034
|
-
if (nodes.length > 0 && edges.length > 0) {
|
|
3035
|
-
const inDegree = new Map(nodes.map((n) => [n.id, 0]));
|
|
3036
|
-
const out = new Map;
|
|
3037
|
-
for (const e of edges) {
|
|
3038
|
-
if (e.source === e.target)
|
|
3039
|
-
continue;
|
|
3040
|
-
if (!inDegree.has(e.source) || !inDegree.has(e.target))
|
|
3041
|
-
continue;
|
|
3042
|
-
inDegree.set(e.target, (inDegree.get(e.target) ?? 0) + 1);
|
|
3043
|
-
const list = out.get(e.source);
|
|
3044
|
-
if (list)
|
|
3045
|
-
list.push(e.target);
|
|
3046
|
-
else
|
|
3047
|
-
out.set(e.source, [e.target]);
|
|
3048
|
-
}
|
|
3049
|
-
const queue = nodes.filter((n) => (inDegree.get(n.id) ?? 0) === 0).map((n) => n.id);
|
|
3050
|
-
let visited = 0;
|
|
3051
|
-
while (queue.length) {
|
|
3052
|
-
const cur = queue.shift();
|
|
3053
|
-
visited++;
|
|
3054
|
-
for (const next of out.get(cur) ?? []) {
|
|
3055
|
-
const d = (inDegree.get(next) ?? 1) - 1;
|
|
3056
|
-
inDegree.set(next, d);
|
|
3057
|
-
if (d === 0)
|
|
3058
|
-
queue.push(next);
|
|
3059
|
-
}
|
|
3060
|
-
}
|
|
3061
|
-
if (visited < nodes.length) {
|
|
3062
|
-
const stuck = nodes.filter((n) => (inDegree.get(n.id) ?? 0) > 0);
|
|
3063
|
-
const names = stuck.slice(0, 3).map(nodeName).join(", ");
|
|
3064
|
-
const more = stuck.length > 3 ? ` +${stuck.length - 3} more` : "";
|
|
3065
|
-
allErrors.push({
|
|
3066
|
-
id: "workflow-cycle",
|
|
3067
|
-
type: "workflow",
|
|
3068
|
-
severity: "error",
|
|
3069
|
-
message: `Circular connection detected involving: ${names}${more}. Break the loop — execution order is undefined inside a cycle.`,
|
|
3070
|
-
nodeId: stuck[0]?.id
|
|
3071
|
-
});
|
|
3072
|
-
}
|
|
3073
|
-
}
|
|
3074
|
-
const isTrigger = (n) => n.type === "trigger" || String(n.actionDefinitionId ?? "").endsWith("_trigger");
|
|
3075
|
-
const triggerIds = nodes.filter(isTrigger).map((n) => n.id);
|
|
3076
|
-
const adjacency = new Map;
|
|
3077
|
-
for (const e of edges) {
|
|
3078
|
-
const list = adjacency.get(e.source);
|
|
3079
|
-
if (list)
|
|
3080
|
-
list.push(e.target);
|
|
3081
|
-
else
|
|
3082
|
-
adjacency.set(e.source, [e.target]);
|
|
3083
|
-
}
|
|
3084
|
-
const reachable = new Set(triggerIds);
|
|
3085
|
-
const queue = [...triggerIds];
|
|
3086
|
-
while (queue.length) {
|
|
3087
|
-
const cur = queue.shift();
|
|
3088
|
-
for (const next of adjacency.get(cur) ?? []) {
|
|
3089
|
-
if (!reachable.has(next)) {
|
|
3090
|
-
reachable.add(next);
|
|
3091
|
-
queue.push(next);
|
|
3092
|
-
}
|
|
3093
|
-
}
|
|
3094
|
-
}
|
|
3095
|
-
let changed = true;
|
|
3096
|
-
while (changed) {
|
|
3097
|
-
changed = false;
|
|
3098
|
-
for (const n of nodes) {
|
|
3099
|
-
if (reachable.has(n.id)) {
|
|
3100
|
-
if (n.parentId && !reachable.has(n.parentId)) {
|
|
3101
|
-
reachable.add(n.parentId);
|
|
3102
|
-
changed = true;
|
|
3103
|
-
}
|
|
3104
|
-
} else if (n.parentId && reachable.has(n.parentId)) {
|
|
3105
|
-
reachable.add(n.id);
|
|
3106
|
-
changed = true;
|
|
3107
|
-
}
|
|
3108
|
-
}
|
|
3109
|
-
}
|
|
3110
|
-
nodes.forEach((node) => {
|
|
3111
|
-
if (isTrigger(node))
|
|
3112
|
-
return;
|
|
3113
|
-
const isConnected = edges.some((e) => e.source === node.id || e.target === node.id);
|
|
3114
|
-
const wontRun = triggerIds.length > 0 ? !reachable.has(node.id) : !isConnected;
|
|
3115
|
-
if (wontRun) {
|
|
3116
|
-
allErrors.push({
|
|
3117
|
-
id: `node-${node.id}-orphan`,
|
|
3118
|
-
type: "node",
|
|
3119
|
-
severity: "warning",
|
|
3120
|
-
message: isConnected ? `"${nodeName(node)}" isn't reachable from a trigger and won't run.` : `"${nodeName(node)}" isn't connected to a flow and won't run.`,
|
|
3121
|
-
nodeId: node.id
|
|
3122
|
-
});
|
|
3123
|
-
}
|
|
3124
|
-
});
|
|
3125
|
-
if (triggerIds.length > 1) {
|
|
3126
|
-
const parentOf = new Map(nodes.filter((n) => n.parentId).map((n) => [n.id, n.parentId]));
|
|
3127
|
-
const undirected = new Map;
|
|
3128
|
-
const link = (a, b) => {
|
|
3129
|
-
const la = undirected.get(a);
|
|
3130
|
-
if (la)
|
|
3131
|
-
la.push(b);
|
|
3132
|
-
else
|
|
3133
|
-
undirected.set(a, [b]);
|
|
3134
|
-
};
|
|
3135
|
-
for (const e of edges) {
|
|
3136
|
-
link(e.source, e.target);
|
|
3137
|
-
link(e.target, e.source);
|
|
3138
|
-
}
|
|
3139
|
-
for (const [child, parent] of parentOf) {
|
|
3140
|
-
link(child, parent);
|
|
3141
|
-
link(parent, child);
|
|
3142
|
-
}
|
|
3143
|
-
const seen = new Set;
|
|
3144
|
-
let flowsWithTrigger = 0;
|
|
3145
|
-
for (const t of triggerIds) {
|
|
3146
|
-
if (seen.has(t))
|
|
3147
|
-
continue;
|
|
3148
|
-
flowsWithTrigger++;
|
|
3149
|
-
const stack = [t];
|
|
3150
|
-
seen.add(t);
|
|
3151
|
-
while (stack.length) {
|
|
3152
|
-
const cur = stack.pop();
|
|
3153
|
-
for (const next of undirected.get(cur) ?? []) {
|
|
3154
|
-
if (!seen.has(next)) {
|
|
3155
|
-
seen.add(next);
|
|
3156
|
-
stack.push(next);
|
|
3157
|
-
}
|
|
3158
|
-
}
|
|
3159
|
-
}
|
|
3160
|
-
}
|
|
3161
|
-
if (flowsWithTrigger > 1) {
|
|
3162
|
-
allErrors.push({
|
|
3163
|
-
id: "workflow-multi-flow",
|
|
3164
|
-
type: "workflow",
|
|
3165
|
-
severity: "warning",
|
|
3166
|
-
message: `This canvas holds ${flowsWithTrigger} separate flows. Each trigger runs only its own flow — consider splitting them into separate flows for clarity.`
|
|
3167
|
-
});
|
|
3168
|
-
}
|
|
3169
|
-
}
|
|
3170
|
-
const errors = allErrors.filter((e) => e.severity === "error");
|
|
3171
|
-
const warnings = allErrors.filter((e) => e.severity === "warning");
|
|
3172
|
-
return {
|
|
3173
|
-
isValid: errors.length === 0,
|
|
3174
|
-
errors,
|
|
3175
|
-
warnings
|
|
3176
|
-
};
|
|
3177
|
-
}
|
|
3178
|
-
function getNodeValidationStatus(nodeId, validationResult) {
|
|
3179
|
-
const hasError = validationResult.errors.some((e) => e.nodeId === nodeId);
|
|
3180
|
-
if (hasError)
|
|
3181
|
-
return "error";
|
|
3182
|
-
const hasWarning = validationResult.warnings.some((e) => e.nodeId === nodeId);
|
|
3183
|
-
if (hasWarning)
|
|
3184
|
-
return "warning";
|
|
3185
|
-
return "valid";
|
|
3186
|
-
}
|
|
3187
|
-
function getEdgeValidationStatus(edgeId, validationResult) {
|
|
3188
|
-
const hasError = validationResult.errors.some((e) => e.edgeId === edgeId);
|
|
3189
|
-
if (hasError)
|
|
3190
|
-
return "error";
|
|
3191
|
-
const hasWarning = validationResult.warnings.some((e) => e.edgeId === edgeId);
|
|
3192
|
-
if (hasWarning)
|
|
3193
|
-
return "warning";
|
|
3194
|
-
return "valid";
|
|
3195
|
-
}
|
|
3196
|
-
// src/catalog/logic/condition/config.ts
|
|
3197
|
-
var CONDITION_OPERATORS = [
|
|
3198
|
-
{ id: "equals", label: "Equals", needsValue: true },
|
|
3199
|
-
{ id: "not_equals", label: "Not Equals", needsValue: true },
|
|
3200
|
-
{ id: "greater_than", label: "Greater Than", needsValue: true },
|
|
3201
|
-
{ id: "greater_than_equals", label: "Greater Than or Equals", needsValue: true },
|
|
3202
|
-
{ id: "less_than", label: "Less Than", needsValue: true },
|
|
3203
|
-
{ id: "less_than_equals", label: "Less Than or Equals", needsValue: true },
|
|
3204
|
-
{ id: "like", label: "Like", needsValue: true },
|
|
3205
|
-
{ id: "contains", label: "Contains", needsValue: true },
|
|
3206
|
-
{ id: "starts_with", label: "Starts With", needsValue: true },
|
|
3207
|
-
{ id: "ends_with", label: "Ends With", needsValue: true },
|
|
3208
|
-
{ id: "is_null", label: "Is Null", needsValue: false },
|
|
3209
|
-
{ id: "is_not_null", label: "Is Not Null", needsValue: false },
|
|
3210
|
-
{ id: "is_empty", label: "Is Empty", needsValue: false }
|
|
3211
|
-
];
|
|
3212
|
-
function isMissingValue(value) {
|
|
3213
|
-
return value === undefined || value === null || value === "";
|
|
3214
|
-
}
|
|
3215
|
-
var ConditionConfig = {
|
|
3216
|
-
defaults: {
|
|
3217
|
-
mode: "visual",
|
|
3218
|
-
filters: [],
|
|
3219
|
-
condition: ""
|
|
3220
|
-
},
|
|
3221
|
-
schema: {
|
|
3222
|
-
mode: { type: "select", required: true, options: ["visual", "expression"] },
|
|
3223
|
-
filters: { type: "array", required: false },
|
|
3224
|
-
condition: { type: "expression", required: false }
|
|
3225
|
-
},
|
|
3226
|
-
validate: (config) => {
|
|
3227
|
-
const errors = [];
|
|
3228
|
-
if (config.mode === "expression") {
|
|
3229
|
-
if (!config.condition) {
|
|
3230
|
-
errors.push("Condition expression is required");
|
|
3231
|
-
} else {
|
|
3232
|
-
try {
|
|
3233
|
-
new Function(`return ${config.condition}`);
|
|
3234
|
-
} catch (e) {
|
|
3235
|
-
errors.push(`Invalid expression: ${e instanceof Error ? e.message : "Unknown error"}`);
|
|
3236
|
-
}
|
|
3237
|
-
}
|
|
3238
|
-
} else {
|
|
3239
|
-
if (!config.filters || config.filters.length === 0) {
|
|
3240
|
-
errors.push("At least one condition filter is required");
|
|
3241
|
-
} else {
|
|
3242
|
-
config.filters.forEach((f, i) => {
|
|
3243
|
-
if (!f.field)
|
|
3244
|
-
errors.push(`Filter ${i + 1}: Field is required`);
|
|
3245
|
-
if (!f.operator)
|
|
3246
|
-
errors.push(`Filter ${i + 1}: Operator is required`);
|
|
3247
|
-
const opDef = CONDITION_OPERATORS.find((o) => o.id === f.operator);
|
|
3248
|
-
if (opDef?.needsValue && isMissingValue(f.value)) {
|
|
3249
|
-
errors.push(`Filter ${i + 1}: Value is required`);
|
|
3250
|
-
}
|
|
3251
|
-
});
|
|
3252
|
-
}
|
|
3253
|
-
}
|
|
3254
|
-
return { valid: errors.length === 0, errors };
|
|
3255
|
-
},
|
|
3256
|
-
merge: (userConfig) => {
|
|
3257
|
-
return { ...ConditionConfig.defaults, ...userConfig };
|
|
3258
|
-
},
|
|
3259
|
-
generateName: (filters) => {
|
|
3260
|
-
if (filters.length === 0)
|
|
3261
|
-
return "If …";
|
|
3262
|
-
const parts = filters.map((f, i) => {
|
|
3263
|
-
const opLabel = CONDITION_OPERATORS.find((o) => o.id === f.operator)?.label || f.operator;
|
|
3264
|
-
const base = `${f.field || "?"} ${opLabel}${f.value ? ` ${f.value}` : ""}`;
|
|
3265
|
-
if (i === 0)
|
|
3266
|
-
return base;
|
|
3267
|
-
return `${(f.logicalOperator || "and").toUpperCase()} ${base}`;
|
|
3268
|
-
});
|
|
3269
|
-
return `If ${parts.join(" ")}`;
|
|
3270
|
-
},
|
|
3271
|
-
getLabel: (config) => {
|
|
3272
|
-
if (config.mode === "expression")
|
|
3273
|
-
return `If ${config.condition || "…"}`;
|
|
3274
|
-
return ConditionConfig.generateName(config.filters);
|
|
3275
|
-
},
|
|
3276
|
-
createFilterId: () => `cond_${Date.now()}_${randomSuffix(5)}`
|
|
3277
|
-
};
|
|
3278
|
-
// src/catalog/logic/for-each/config.ts
|
|
3279
|
-
var SOURCELESS_MODES = new Set(["range", "cursor", "while"]);
|
|
3280
|
-
var ForEachConfig = {
|
|
3281
|
-
defaults: {
|
|
3282
|
-
arrayPath: "",
|
|
3283
|
-
iterationMode: "records",
|
|
3284
|
-
itemVariable: "item",
|
|
3285
|
-
indexVariable: "index",
|
|
3286
|
-
batchSize: 0,
|
|
3287
|
-
continueOnError: false,
|
|
3288
|
-
concurrency: "sequential",
|
|
3289
|
-
collect: "array"
|
|
3290
|
-
},
|
|
3291
|
-
schema: {
|
|
3292
|
-
arrayPath: {
|
|
3293
|
-
type: "select",
|
|
3294
|
-
required: false,
|
|
3295
|
-
dynamic: true,
|
|
3296
|
-
description: "A path to an array in the upstream step's output (the Query's object name, or a field holding a list) — the values come from that step's data, not from the catalog"
|
|
3297
|
-
},
|
|
3298
|
-
iterationMode: {
|
|
3299
|
-
type: "select",
|
|
3300
|
-
required: false,
|
|
3301
|
-
options: ["records", "fields", "range", "nested", "batch", "cursor", "while", "array"],
|
|
3302
|
-
description: "What one iteration is: records (one per record, the default; array is its legacy alias), fields, range, nested, batch, cursor or while"
|
|
3303
|
-
},
|
|
3304
|
-
itemVariable: { type: "string", required: true },
|
|
3305
|
-
indexVariable: { type: "string", required: false },
|
|
3306
|
-
batchSize: { type: "number", required: false },
|
|
3307
|
-
continueOnError: { type: "boolean", required: false },
|
|
3308
|
-
collect: {
|
|
3309
|
-
type: "select",
|
|
3310
|
-
required: false,
|
|
3311
|
-
options: ["array", "none", "groupBy", "reduce", "rows"],
|
|
3312
|
-
description: "What the Done port carries besides the counts: array (each iteration's result in `results`, the default), none, groupBy (by groupByPath), " + "reduce (reduceOp over reducePath), or rows — also `data`: every row the body produced (each iteration's Query/Transform rows, else its result), " + "so ONE step on the Done port maps and writes them all instead of a write per iteration."
|
|
3313
|
-
}
|
|
3314
|
-
},
|
|
3315
|
-
validate: (config) => {
|
|
3316
|
-
const errors = [];
|
|
3317
|
-
const mode = config.iterationMode || "records";
|
|
3318
|
-
if (mode === "range") {
|
|
3319
|
-
if (!config.count || config.count <= 0) {
|
|
3320
|
-
errors.push("Range mode needs a positive iteration count");
|
|
3321
|
-
}
|
|
3322
|
-
} else if (mode === "nested") {
|
|
3323
|
-
if (!config.arrayPath)
|
|
3324
|
-
errors.push("Select the records to drill into");
|
|
3325
|
-
if (!config.nestedPath)
|
|
3326
|
-
errors.push('Nested mode needs the sub-array path to iterate (e.g. "lineItems")');
|
|
3327
|
-
} else if (!SOURCELESS_MODES.has(mode)) {
|
|
3328
|
-
if (!config.arrayPath) {
|
|
3329
|
-
errors.push("Array path is required — select which records to iterate over");
|
|
3330
|
-
}
|
|
3331
|
-
}
|
|
3332
|
-
if (config.concurrency === "parallel" && config.maxConcurrency !== undefined && config.maxConcurrency < 1) {
|
|
3333
|
-
errors.push("Max concurrency must be at least 1");
|
|
3334
|
-
}
|
|
3335
|
-
if (config.batchSize !== undefined && config.batchSize < 0) {
|
|
3336
|
-
errors.push("Batch size must be 0 (all) or a positive number");
|
|
3337
|
-
}
|
|
3338
|
-
if (config.collect === "groupBy" && !config.groupByPath) {
|
|
3339
|
-
errors.push("Group-by collection needs a field path to group on");
|
|
3340
|
-
}
|
|
3341
|
-
return { valid: errors.length === 0, errors };
|
|
3342
|
-
},
|
|
3343
|
-
merge: (userConfig) => {
|
|
3344
|
-
return { ...ForEachConfig.defaults, ...userConfig };
|
|
3345
|
-
},
|
|
3346
|
-
getLabel: (config) => {
|
|
3347
|
-
const mode = config.iterationMode || "records";
|
|
3348
|
-
if (mode === "range")
|
|
3349
|
-
return `ForEach ×${config.count ?? "…"}`;
|
|
3350
|
-
if (mode === "cursor")
|
|
3351
|
-
return "ForEach (paginate)";
|
|
3352
|
-
if (mode === "while")
|
|
3353
|
-
return "ForEach (while)";
|
|
3354
|
-
return `ForEach in ${config.arrayPath || "…"}`;
|
|
3355
|
-
},
|
|
3356
|
-
generateName: (arrayPath) => {
|
|
3357
|
-
return `ForEach in ${arrayPath || "…"}`;
|
|
3358
|
-
}
|
|
3359
|
-
};
|
|
3360
|
-
// src/catalog/logic/for-each/pathOptions.ts
|
|
3361
|
-
function inferSchemaFromSample(sample) {
|
|
3362
|
-
const properties = {};
|
|
3363
|
-
for (const [key, value] of Object.entries(sample)) {
|
|
3364
|
-
if (value === null || value === undefined) {
|
|
3365
|
-
properties[key] = { type: "string" };
|
|
3366
|
-
} else if (Array.isArray(value)) {
|
|
3367
|
-
if (value.length > 0 && typeof value[0] === "object" && value[0] !== null) {
|
|
3368
|
-
properties[key] = { type: "array", items: inferSchemaFromSample(value[0]) };
|
|
3369
|
-
} else {
|
|
3370
|
-
properties[key] = { type: "array" };
|
|
3371
|
-
}
|
|
3372
|
-
} else if (typeof value === "object") {
|
|
3373
|
-
properties[key] = inferSchemaFromSample(value);
|
|
3374
|
-
} else if (typeof value === "number") {
|
|
3375
|
-
properties[key] = { type: "number" };
|
|
3376
|
-
} else if (typeof value === "boolean") {
|
|
3377
|
-
properties[key] = { type: "boolean" };
|
|
3378
|
-
} else {
|
|
3379
|
-
properties[key] = { type: "string" };
|
|
3380
|
-
}
|
|
3381
|
-
}
|
|
3382
|
-
return { type: "object", properties };
|
|
3383
|
-
}
|
|
3384
|
-
function collectArrayPaths(schema, prefix, add, depth = 0) {
|
|
3385
|
-
if (!schema || typeof schema !== "object" || depth > 8)
|
|
3386
|
-
return;
|
|
3387
|
-
if (schema.type === "array") {
|
|
3388
|
-
if (prefix) {
|
|
3389
|
-
const itemsType = schema.items?.type;
|
|
3390
|
-
const label = itemsType === "object" ? "array of objects" : "array";
|
|
3391
|
-
add(prefix, `${prefix} (${label})`, `Iterate over "${prefix}"`);
|
|
3392
|
-
}
|
|
3393
|
-
if (schema.items) {
|
|
3394
|
-
collectArrayPaths(schema.items, prefix, add, depth + 1);
|
|
3395
|
-
}
|
|
3396
|
-
return;
|
|
3397
|
-
}
|
|
3398
|
-
if (schema.type === "object" && schema.properties) {
|
|
3399
|
-
for (const [key, child] of Object.entries(schema.properties)) {
|
|
3400
|
-
const next = prefix ? `${prefix}.${key}` : key;
|
|
3401
|
-
collectArrayPaths(child, next, add, depth + 1);
|
|
3402
|
-
}
|
|
3403
|
-
}
|
|
3404
|
-
}
|
|
3405
|
-
function collectArrayPathsFromSample(sample, prefix, add, depth = 0) {
|
|
3406
|
-
if (!sample || typeof sample !== "object" || depth > 8)
|
|
3407
|
-
return;
|
|
3408
|
-
if (Array.isArray(sample)) {
|
|
3409
|
-
if (prefix) {
|
|
3410
|
-
const itemsObj = sample.length > 0 && typeof sample[0] === "object" && sample[0] !== null;
|
|
3411
|
-
const label = itemsObj ? "array of objects" : "array";
|
|
3412
|
-
add(prefix, `${prefix} (${label})`, `Iterate over "${prefix}"`);
|
|
3413
|
-
}
|
|
3414
|
-
if (sample.length > 0) {
|
|
3415
|
-
collectArrayPathsFromSample(sample[0], prefix, add, depth + 1);
|
|
3416
|
-
}
|
|
3417
|
-
return;
|
|
3418
|
-
}
|
|
3419
|
-
for (const [key, value] of Object.entries(sample)) {
|
|
3420
|
-
const next = prefix ? `${prefix}.${key}` : key;
|
|
3421
|
-
collectArrayPathsFromSample(value, next, add, depth + 1);
|
|
3422
|
-
}
|
|
3423
|
-
}
|
|
3424
|
-
function buildPathOptions(upstreamNodes) {
|
|
3425
|
-
const paths = [];
|
|
3426
|
-
const seen = new Set;
|
|
3427
|
-
const add = (id, text, description) => {
|
|
3428
|
-
if (seen.has(id))
|
|
3429
|
-
return;
|
|
3430
|
-
seen.add(id);
|
|
3431
|
-
paths.push({ id, text, description });
|
|
3432
|
-
};
|
|
3433
|
-
if (upstreamNodes) {
|
|
3434
|
-
for (const node of upstreamNodes) {
|
|
3435
|
-
const label = node.label || node.type || "Upstream";
|
|
3436
|
-
const queryTable = node.config?.visualConfig?.tables?.[0];
|
|
3437
|
-
add("data", queryTable ? `${queryTable} (${label})` : `All records (${label})`, queryTable ? `Iterate over every ${queryTable} record` : "Iterate over every record from upstream");
|
|
3438
|
-
const schema = node.outputSchema || node.config?._outputSchema;
|
|
3439
|
-
if (schema)
|
|
3440
|
-
collectArrayPaths(schema, "", add);
|
|
3441
|
-
const output = node.executionData?.output;
|
|
3442
|
-
if (output) {
|
|
3443
|
-
const records = Array.isArray(output.data) ? output.data : Array.isArray(output) ? output : null;
|
|
3444
|
-
if (records && records.length > 0 && typeof records[0] === "object") {
|
|
3445
|
-
collectArrayPathsFromSample(records[0], "", add);
|
|
3446
|
-
}
|
|
3447
|
-
}
|
|
3448
|
-
}
|
|
3449
|
-
}
|
|
3450
|
-
if (paths.length === 0) {
|
|
3451
|
-
add("data", "data (upstream output)");
|
|
3452
|
-
}
|
|
3453
|
-
return paths;
|
|
2867
|
+
// src/catalog/actions/http-request/config.ts
|
|
2868
|
+
function urlCheckedAtRunTime(url, connectionId) {
|
|
2869
|
+
const u = url.trim();
|
|
2870
|
+
if (u.startsWith("{{"))
|
|
2871
|
+
return true;
|
|
2872
|
+
const hasScheme = /^[a-z][a-z0-9+.-]*:/i.test(u);
|
|
2873
|
+
return !!connectionId && !hasScheme;
|
|
3454
2874
|
}
|
|
3455
|
-
|
|
3456
|
-
var RESERVED_KEYS = new Set(["_lineage", "_meta", "_redacted", "_error"]);
|
|
3457
|
-
var KEY_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
3458
|
-
var VariablesConfig = {
|
|
3459
|
-
schema: {
|
|
3460
|
-
fields: { type: "array", required: true }
|
|
3461
|
-
},
|
|
2875
|
+
var HTTPRequestConfig = {
|
|
3462
2876
|
defaults: {
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
if (seen.has(f.key)) {
|
|
3483
|
-
errors.push(`Duplicate key "${f.key}".`);
|
|
3484
|
-
continue;
|
|
3485
|
-
}
|
|
3486
|
-
seen.add(f.key);
|
|
3487
|
-
if (!f.type)
|
|
3488
|
-
errors.push(`"${f.key}" is missing a type.`);
|
|
3489
|
-
if (!f.source)
|
|
3490
|
-
errors.push(`"${f.key}" is missing a source.`);
|
|
3491
|
-
}
|
|
3492
|
-
return { valid: errors.length === 0, errors };
|
|
2877
|
+
apiType: "rest",
|
|
2878
|
+
method: "GET",
|
|
2879
|
+
url: "",
|
|
2880
|
+
queryParams: [],
|
|
2881
|
+
headers: [],
|
|
2882
|
+
bodyType: "json",
|
|
2883
|
+
body: undefined,
|
|
2884
|
+
formData: [],
|
|
2885
|
+
graphqlQuery: "",
|
|
2886
|
+
graphqlVariables: {},
|
|
2887
|
+
auth: { type: "none" },
|
|
2888
|
+
timeout: 30000,
|
|
2889
|
+
failOnError: true,
|
|
2890
|
+
followRedirects: true,
|
|
2891
|
+
retry: { maxAttempts: 1, retryOn: [429, 500, 502, 503, 504], initialDelayMs: 1000, backoffFactor: 2, maxDelayMs: 30000 },
|
|
2892
|
+
pagination: { type: "none" },
|
|
2893
|
+
extract: {},
|
|
2894
|
+
responseType: "json",
|
|
2895
|
+
expectedContentTypes: []
|
|
3493
2896
|
},
|
|
3494
|
-
computeOutputSchema(fields) {
|
|
3495
|
-
const properties = {};
|
|
3496
|
-
for (const f of fields) {
|
|
3497
|
-
properties[f.key] = fieldToJsonSchema(f);
|
|
3498
|
-
}
|
|
3499
|
-
return { type: "object", properties };
|
|
3500
|
-
}
|
|
3501
|
-
};
|
|
3502
|
-
function fieldToJsonSchema(f) {
|
|
3503
|
-
const node = { type: f.type };
|
|
3504
|
-
if (f.description)
|
|
3505
|
-
node.description = f.description;
|
|
3506
|
-
if (f.validation?.enum)
|
|
3507
|
-
node.enum = f.validation.enum;
|
|
3508
|
-
return node;
|
|
3509
|
-
}
|
|
3510
|
-
// src/catalog/utility/debug/config.ts
|
|
3511
|
-
var DebugConfig = {
|
|
3512
2897
|
schema: {
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
}
|
|
3555
|
-
break;
|
|
3556
|
-
case "assert":
|
|
3557
|
-
if (!config.assert?.predicate?.trim())
|
|
3558
|
-
errors.push("Assert mode: predicate is required.");
|
|
3559
|
-
if (!config.assert?.failureMessage?.trim())
|
|
3560
|
-
errors.push("Assert mode: failure message is required.");
|
|
3561
|
-
break;
|
|
3562
|
-
case "profile":
|
|
3563
|
-
if (!config.profile?.scope)
|
|
3564
|
-
errors.push("Profile mode: scope is required.");
|
|
3565
|
-
break;
|
|
2898
|
+
apiType: { type: "select", required: false, options: ["rest", "graphql"] },
|
|
2899
|
+
baseUrl: { type: "string", required: false },
|
|
2900
|
+
queryParams: { type: "array", required: false },
|
|
2901
|
+
bodyType: { type: "select", required: false, options: ["none", "json", "raw", "form-data", "xml"] },
|
|
2902
|
+
formData: { type: "array", required: false },
|
|
2903
|
+
graphqlQuery: { type: "code", required: false },
|
|
2904
|
+
graphqlVariables: { type: "json", required: false },
|
|
2905
|
+
auth: { type: "object", required: false },
|
|
2906
|
+
failOnError: { type: "boolean", required: false },
|
|
2907
|
+
followRedirects: { type: "boolean", required: false },
|
|
2908
|
+
retry: { type: "object", required: false },
|
|
2909
|
+
pagination: { type: "object", required: false },
|
|
2910
|
+
extract: { type: "object", required: false },
|
|
2911
|
+
responseType: { type: "select", required: false, options: ["json", "text", "binary"] },
|
|
2912
|
+
expectedContentTypes: { type: "array", required: false },
|
|
2913
|
+
method: {
|
|
2914
|
+
type: "select",
|
|
2915
|
+
required: true,
|
|
2916
|
+
options: ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
|
|
2917
|
+
description: "HTTP method to use"
|
|
2918
|
+
},
|
|
2919
|
+
url: {
|
|
2920
|
+
type: "url",
|
|
2921
|
+
required: true,
|
|
2922
|
+
description: "The URL to send the request to"
|
|
2923
|
+
},
|
|
2924
|
+
headers: {
|
|
2925
|
+
type: "json",
|
|
2926
|
+
required: false,
|
|
2927
|
+
description: "HTTP headers to send with the request"
|
|
2928
|
+
},
|
|
2929
|
+
body: {
|
|
2930
|
+
type: "json",
|
|
2931
|
+
required: false,
|
|
2932
|
+
description: "Request body (for POST, PUT, PATCH)"
|
|
2933
|
+
},
|
|
2934
|
+
timeout: {
|
|
2935
|
+
type: "number",
|
|
2936
|
+
required: false,
|
|
2937
|
+
description: "Request timeout in milliseconds",
|
|
2938
|
+
min: 1000
|
|
3566
2939
|
}
|
|
3567
|
-
return { valid: errors.length === 0, errors };
|
|
3568
2940
|
},
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
2941
|
+
validate: (config) => {
|
|
2942
|
+
const errors = [];
|
|
2943
|
+
const apiType = config.apiType ?? "rest";
|
|
2944
|
+
if (apiType === "rest" && !config.method) {
|
|
2945
|
+
errors.push("Method is required");
|
|
3573
2946
|
}
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
2947
|
+
if (apiType === "graphql" && !config.graphqlQuery?.trim()) {
|
|
2948
|
+
errors.push("GraphQL query is required");
|
|
2949
|
+
}
|
|
2950
|
+
if (!config.url) {
|
|
2951
|
+
errors.push("URL is required");
|
|
2952
|
+
} else if (typeof config.url === "string" && !urlCheckedAtRunTime(config.url, config.connectionId)) {
|
|
2953
|
+
try {
|
|
2954
|
+
new URL(config.url);
|
|
2955
|
+
} catch {
|
|
2956
|
+
errors.push("Invalid URL format");
|
|
3581
2957
|
}
|
|
2958
|
+
}
|
|
2959
|
+
if (config.timeout !== undefined && typeof config.timeout === "number") {
|
|
2960
|
+
if (config.timeout < 1000) {
|
|
2961
|
+
errors.push("Timeout must be at least 1000ms");
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
return {
|
|
2965
|
+
valid: errors.length === 0,
|
|
2966
|
+
errors
|
|
3582
2967
|
};
|
|
3583
2968
|
},
|
|
3584
|
-
merge(userConfig) {
|
|
3585
|
-
return {
|
|
2969
|
+
merge: (userConfig) => {
|
|
2970
|
+
return {
|
|
2971
|
+
...HTTPRequestConfig.defaults,
|
|
2972
|
+
...userConfig
|
|
2973
|
+
};
|
|
3586
2974
|
},
|
|
3587
|
-
getLabel(config) {
|
|
3588
|
-
|
|
2975
|
+
getLabel: (config) => {
|
|
2976
|
+
const type = config.apiType === "graphql" ? "GraphQL" : config.method ?? "GET";
|
|
2977
|
+
return `${type} ${config.url}`;
|
|
3589
2978
|
}
|
|
3590
2979
|
};
|
|
3591
|
-
|
|
3592
|
-
var LEGACY_UNITS = new Set([
|
|
3593
|
-
"milliseconds",
|
|
3594
|
-
"seconds",
|
|
3595
|
-
"minutes",
|
|
3596
|
-
"hours"
|
|
3597
|
-
]);
|
|
3598
|
-
function migrateDelayConfig(raw) {
|
|
3599
|
-
if (raw && typeof raw === "object" && "mode" in raw) {
|
|
3600
|
-
return raw;
|
|
3601
|
-
}
|
|
3602
|
-
const legacy = raw ?? {};
|
|
3603
|
-
const duration = typeof legacy.duration === "number" && Number.isFinite(legacy.duration) ? legacy.duration : 1;
|
|
3604
|
-
const unit = typeof legacy.unit === "string" && LEGACY_UNITS.has(legacy.unit) ? legacy.unit : "milliseconds";
|
|
3605
|
-
return {
|
|
3606
|
-
mode: "fixed",
|
|
3607
|
-
fixed: { duration, unit }
|
|
3608
|
-
};
|
|
3609
|
-
}
|
|
2980
|
+
|
|
3610
2981
|
// src/catalog/communication/email-send/config.ts
|
|
3611
2982
|
var EmailConfig = {
|
|
3612
2983
|
defaults: {
|
|
@@ -3645,43 +3016,199 @@ var EmailConfig = {
|
|
|
3645
3016
|
validate: (config) => {
|
|
3646
3017
|
const errors = [];
|
|
3647
3018
|
if (!config.connection)
|
|
3648
|
-
errors.push("Email service connection is required");
|
|
3649
|
-
if (!config.operation)
|
|
3650
|
-
errors.push("Operation is required");
|
|
3651
|
-
if (config.operation === "send") {
|
|
3652
|
-
if (!config.to)
|
|
3653
|
-
errors.push("Recipient email is required for send operation");
|
|
3654
|
-
if (!config.subject)
|
|
3655
|
-
errors.push("Subject is required for send operation");
|
|
3656
|
-
if (!config.body)
|
|
3657
|
-
errors.push("Body is required for send operation");
|
|
3658
|
-
} else if (config.operation) {
|
|
3659
|
-
errors.push("Only Send is available for the Email action right now");
|
|
3019
|
+
errors.push("Email service connection is required");
|
|
3020
|
+
if (!config.operation)
|
|
3021
|
+
errors.push("Operation is required");
|
|
3022
|
+
if (config.operation === "send") {
|
|
3023
|
+
if (!config.to)
|
|
3024
|
+
errors.push("Recipient email is required for send operation");
|
|
3025
|
+
if (!config.subject)
|
|
3026
|
+
errors.push("Subject is required for send operation");
|
|
3027
|
+
if (!config.body)
|
|
3028
|
+
errors.push("Body is required for send operation");
|
|
3029
|
+
} else if (config.operation) {
|
|
3030
|
+
errors.push("Only Send is available for the Email action right now");
|
|
3031
|
+
}
|
|
3032
|
+
return { valid: errors.length === 0, errors };
|
|
3033
|
+
},
|
|
3034
|
+
merge: (userConfig) => {
|
|
3035
|
+
return { ...EmailConfig.defaults, ...userConfig };
|
|
3036
|
+
},
|
|
3037
|
+
getLabel: (config) => {
|
|
3038
|
+
switch (config.operation) {
|
|
3039
|
+
case "send":
|
|
3040
|
+
return `Email: Send to ${config.to}`;
|
|
3041
|
+
case "read":
|
|
3042
|
+
return `Email: Read from ${config.folder}`;
|
|
3043
|
+
case "search":
|
|
3044
|
+
return `Email: Search "${config.query}"`;
|
|
3045
|
+
case "delete":
|
|
3046
|
+
return `Email: Delete from ${config.folder}`;
|
|
3047
|
+
case "move":
|
|
3048
|
+
return `Email: Move to ${config.folder}`;
|
|
3049
|
+
case "mark_read":
|
|
3050
|
+
return `Email: Mark as read`;
|
|
3051
|
+
default:
|
|
3052
|
+
return `Email: ${config.operation}`;
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
};
|
|
3056
|
+
|
|
3057
|
+
// src/catalog/communication/google-chat/config.ts
|
|
3058
|
+
var GoogleChatConfig = {
|
|
3059
|
+
defaults: { connection: "", space: "", spaceName: "", text: "", threadKey: "" },
|
|
3060
|
+
schema: {
|
|
3061
|
+
connection: { type: "connection", required: true },
|
|
3062
|
+
space: { type: "string", required: true },
|
|
3063
|
+
text: { type: "string", required: false },
|
|
3064
|
+
threadKey: { type: "string", required: false }
|
|
3065
|
+
},
|
|
3066
|
+
validate: (config) => {
|
|
3067
|
+
const errors = [];
|
|
3068
|
+
if (!config.connection)
|
|
3069
|
+
errors.push("Connect a Google Chat account");
|
|
3070
|
+
if (!config.space)
|
|
3071
|
+
errors.push("Pick a space");
|
|
3072
|
+
if (!config.text && !config.message)
|
|
3073
|
+
errors.push("Add a message");
|
|
3074
|
+
return { valid: errors.length === 0, errors };
|
|
3075
|
+
},
|
|
3076
|
+
merge: (userConfig) => ({
|
|
3077
|
+
...GoogleChatConfig.defaults,
|
|
3078
|
+
...userConfig
|
|
3079
|
+
}),
|
|
3080
|
+
getLabel: () => "Google Chat"
|
|
3081
|
+
};
|
|
3082
|
+
|
|
3083
|
+
// src/catalog/communication/slack-message/config.ts
|
|
3084
|
+
var SlackMessageConfig = {
|
|
3085
|
+
defaults: { connection: "", messageType: "channel", channel: "", channelName: "", text: "", blocks: undefined },
|
|
3086
|
+
schema: {
|
|
3087
|
+
connection: { type: "connection", required: true },
|
|
3088
|
+
messageType: { type: "string", required: false },
|
|
3089
|
+
channel: { type: "string", required: true },
|
|
3090
|
+
text: { type: "string", required: false },
|
|
3091
|
+
blocks: { type: "json", required: false }
|
|
3092
|
+
},
|
|
3093
|
+
validate: (config) => {
|
|
3094
|
+
const errors = [];
|
|
3095
|
+
if (!config.connection)
|
|
3096
|
+
errors.push("Slack workspace connection is required");
|
|
3097
|
+
if (!config.channel)
|
|
3098
|
+
errors.push(config.messageType === "user" ? "A user is required" : "A channel is required");
|
|
3099
|
+
const hasText = Boolean(config.text || config.message);
|
|
3100
|
+
const hasBlocks = Array.isArray(config.blocks) && config.blocks.length > 0;
|
|
3101
|
+
if (!hasText && !hasBlocks)
|
|
3102
|
+
errors.push("Add a message or at least one block");
|
|
3103
|
+
return { valid: errors.length === 0, errors };
|
|
3104
|
+
},
|
|
3105
|
+
merge: (userConfig) => {
|
|
3106
|
+
return { ...SlackMessageConfig.defaults, ...userConfig };
|
|
3107
|
+
},
|
|
3108
|
+
getLabel: (config) => {
|
|
3109
|
+
const prefix = config.messageType === "user" ? "@" : "#";
|
|
3110
|
+
return config.channelName ? `Slack: ${prefix}${config.channelName}` : config.channel ? `Slack: ${config.channel}` : "Slack";
|
|
3111
|
+
}
|
|
3112
|
+
};
|
|
3113
|
+
|
|
3114
|
+
// src/catalog/communication/teams-message/config.ts
|
|
3115
|
+
var TeamsMessageConfig = {
|
|
3116
|
+
defaults: { connection: "", teamId: "", teamName: "", channelId: "", channelName: "", title: "", message: "" },
|
|
3117
|
+
schema: {
|
|
3118
|
+
connection: { type: "connection", required: true },
|
|
3119
|
+
teamId: { type: "string", required: true },
|
|
3120
|
+
channelId: { type: "string", required: true },
|
|
3121
|
+
title: { type: "string", required: false },
|
|
3122
|
+
message: { type: "string", required: true }
|
|
3123
|
+
},
|
|
3124
|
+
validate: (config) => {
|
|
3125
|
+
const errors = [];
|
|
3126
|
+
if (!config.connection)
|
|
3127
|
+
errors.push("Connect a Microsoft Teams account");
|
|
3128
|
+
if (!config.teamId)
|
|
3129
|
+
errors.push("Pick a team");
|
|
3130
|
+
if (!config.channelId)
|
|
3131
|
+
errors.push("Pick a channel");
|
|
3132
|
+
if (!config.message)
|
|
3133
|
+
errors.push("Add a message");
|
|
3134
|
+
return { valid: errors.length === 0, errors };
|
|
3135
|
+
},
|
|
3136
|
+
merge: (userConfig) => {
|
|
3137
|
+
return { ...TeamsMessageConfig.defaults, ...userConfig };
|
|
3138
|
+
},
|
|
3139
|
+
getLabel: (config) => config.channelName ? `Teams: #${config.channelName}` : "Teams"
|
|
3140
|
+
};
|
|
3141
|
+
|
|
3142
|
+
// src/catalog/communication/webhook-call/config.ts
|
|
3143
|
+
var WebhookCallConfig = {
|
|
3144
|
+
defaults: {
|
|
3145
|
+
url: "",
|
|
3146
|
+
method: "POST",
|
|
3147
|
+
payload: {},
|
|
3148
|
+
headers: {}
|
|
3149
|
+
},
|
|
3150
|
+
schema: {
|
|
3151
|
+
url: { type: "url", required: true },
|
|
3152
|
+
method: { type: "select", required: true, options: ["POST", "PUT", "PATCH", "GET"] },
|
|
3153
|
+
payload: { type: "json", required: false },
|
|
3154
|
+
headers: { type: "json", required: false }
|
|
3155
|
+
},
|
|
3156
|
+
validate: (config) => {
|
|
3157
|
+
const errors = [];
|
|
3158
|
+
if (!config.url)
|
|
3159
|
+
errors.push("Webhook URL is required");
|
|
3160
|
+
if (!config.method)
|
|
3161
|
+
errors.push("HTTP method is required");
|
|
3162
|
+
return { valid: errors.length === 0, errors };
|
|
3163
|
+
},
|
|
3164
|
+
merge: (userConfig) => {
|
|
3165
|
+
return { ...WebhookCallConfig.defaults, ...userConfig };
|
|
3166
|
+
},
|
|
3167
|
+
getLabel: (config) => {
|
|
3168
|
+
return `${config.method}: ${config.url.substring(0, 40)}...`;
|
|
3169
|
+
}
|
|
3170
|
+
};
|
|
3171
|
+
|
|
3172
|
+
// src/catalog/communication/whatsapp-send/config.ts
|
|
3173
|
+
var WhatsappSendConfig = {
|
|
3174
|
+
schema: {
|
|
3175
|
+
connection: { type: "connection", required: true },
|
|
3176
|
+
to: { type: "string", required: true },
|
|
3177
|
+
mode: { type: "select", required: true, options: ["template", "text"] },
|
|
3178
|
+
templateName: { type: "string", required: false },
|
|
3179
|
+
templateLanguage: { type: "string", required: false },
|
|
3180
|
+
templateVariables: { type: "array", required: false },
|
|
3181
|
+
text: { type: "string", required: false },
|
|
3182
|
+
previewUrl: { type: "boolean", required: false }
|
|
3183
|
+
},
|
|
3184
|
+
defaults: {
|
|
3185
|
+
connection: "",
|
|
3186
|
+
to: "",
|
|
3187
|
+
mode: "template",
|
|
3188
|
+
templateLanguage: "en_US",
|
|
3189
|
+
templateVariables: []
|
|
3190
|
+
},
|
|
3191
|
+
validate: (config) => {
|
|
3192
|
+
const errors = [];
|
|
3193
|
+
if (!config.connection)
|
|
3194
|
+
errors.push("Select a WhatsApp connection");
|
|
3195
|
+
if (!config.to?.trim())
|
|
3196
|
+
errors.push("Recipient phone number is required");
|
|
3197
|
+
if (config.mode === "text") {
|
|
3198
|
+
if (!config.text?.trim())
|
|
3199
|
+
errors.push("Message text is required");
|
|
3200
|
+
} else {
|
|
3201
|
+
if (!config.templateName?.trim())
|
|
3202
|
+
errors.push("Choose an approved template");
|
|
3660
3203
|
}
|
|
3661
3204
|
return { valid: errors.length === 0, errors };
|
|
3662
3205
|
},
|
|
3663
3206
|
merge: (userConfig) => {
|
|
3664
|
-
return { ...
|
|
3207
|
+
return { ...WhatsappSendConfig.defaults, ...userConfig };
|
|
3665
3208
|
},
|
|
3666
|
-
getLabel: (config) => {
|
|
3667
|
-
switch (config.operation) {
|
|
3668
|
-
case "send":
|
|
3669
|
-
return `Email: Send to ${config.to}`;
|
|
3670
|
-
case "read":
|
|
3671
|
-
return `Email: Read from ${config.folder}`;
|
|
3672
|
-
case "search":
|
|
3673
|
-
return `Email: Search "${config.query}"`;
|
|
3674
|
-
case "delete":
|
|
3675
|
-
return `Email: Delete from ${config.folder}`;
|
|
3676
|
-
case "move":
|
|
3677
|
-
return `Email: Move to ${config.folder}`;
|
|
3678
|
-
case "mark_read":
|
|
3679
|
-
return `Email: Mark as read`;
|
|
3680
|
-
default:
|
|
3681
|
-
return `Email: ${config.operation}`;
|
|
3682
|
-
}
|
|
3683
|
-
}
|
|
3209
|
+
getLabel: (config) => config.mode === "text" ? `WhatsApp to ${config.to || "—"}` : `WhatsApp template "${config.templateName || "—"}" to ${config.to || "—"}`
|
|
3684
3210
|
};
|
|
3211
|
+
|
|
3685
3212
|
// src/catalog/general/transform/config.ts
|
|
3686
3213
|
var TRANSFORM_MODES = ["visual", "expression"];
|
|
3687
3214
|
var TRANSFORM_OPERATIONS = [
|
|
@@ -3839,6 +3366,7 @@ var TransformConfig = {
|
|
|
3839
3366
|
return `map_${Date.now()}_${randomSuffix(5)}`;
|
|
3840
3367
|
}
|
|
3841
3368
|
};
|
|
3369
|
+
|
|
3842
3370
|
// src/catalog/general/aggregate/config.ts
|
|
3843
3371
|
var AGGREGATE_FUNCTIONS = [
|
|
3844
3372
|
"sum",
|
|
@@ -3860,8 +3388,8 @@ var AGGREGATE_FUNCTION_DESCRIPTIONS = {
|
|
|
3860
3388
|
first: "the field's value on the first record",
|
|
3861
3389
|
last: "the field's value on the last record"
|
|
3862
3390
|
};
|
|
3863
|
-
var
|
|
3864
|
-
var
|
|
3391
|
+
var RESERVED_KEYS = new Set(["_meta", "_lineage", "_redacted", "_error"]);
|
|
3392
|
+
var KEY_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
3865
3393
|
var NUMBER_VALUED = new Set(["sum", "avg", "count", "count_distinct"]);
|
|
3866
3394
|
function groupKeyName(path) {
|
|
3867
3395
|
const trimmed = String(path ?? "").trim().replace(/\[\]$/, "");
|
|
@@ -3975,9 +3503,9 @@ var AggregateConfig = {
|
|
|
3975
3503
|
const as = m.as;
|
|
3976
3504
|
if (typeof as !== "string" || as === "") {
|
|
3977
3505
|
errors.push(`${where}: as is required — the output key the value lands under, e.g. "TotalAmount".`);
|
|
3978
|
-
} else if (
|
|
3506
|
+
} else if (RESERVED_KEYS.has(as)) {
|
|
3979
3507
|
errors.push(`"${as}" is reserved — pick another output key.`);
|
|
3980
|
-
} else if (!
|
|
3508
|
+
} else if (!KEY_PATTERN.test(as)) {
|
|
3981
3509
|
errors.push(`"${as}" is not a valid output key (letters, digits, underscore; must not start with a digit).`);
|
|
3982
3510
|
} else if (seen.has(as)) {
|
|
3983
3511
|
errors.push(`Duplicate output key "${as}" — every metric and group-by key lands under its own name.`);
|
|
@@ -3993,7 +3521,7 @@ var AggregateConfig = {
|
|
|
3993
3521
|
});
|
|
3994
3522
|
const outputAs = config.outputAs;
|
|
3995
3523
|
if (outputAs !== undefined && outputAs !== null && outputAs !== "") {
|
|
3996
|
-
if (typeof outputAs !== "string" || !
|
|
3524
|
+
if (typeof outputAs !== "string" || !KEY_PATTERN.test(outputAs)) {
|
|
3997
3525
|
errors.push('outputAs must be a single name (letters, digits, underscore), e.g. "totals" — later steps read {{totals[].<key>}}.');
|
|
3998
3526
|
}
|
|
3999
3527
|
}
|
|
@@ -4027,316 +3555,378 @@ var AggregateConfig = {
|
|
|
4027
3555
|
return parts.length > 0 ? `${parts.join(", ")}${by}` : "Aggregate";
|
|
4028
3556
|
}
|
|
4029
3557
|
};
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
if (cur?.type === "object" && cur.properties)
|
|
4050
|
-
return cur;
|
|
4051
|
-
return;
|
|
3558
|
+
|
|
3559
|
+
// src/catalog/logic/condition/config.ts
|
|
3560
|
+
var CONDITION_OPERATORS = [
|
|
3561
|
+
{ id: "equals", label: "Equals", needsValue: true },
|
|
3562
|
+
{ id: "not_equals", label: "Not Equals", needsValue: true },
|
|
3563
|
+
{ id: "greater_than", label: "Greater Than", needsValue: true },
|
|
3564
|
+
{ id: "greater_than_equals", label: "Greater Than or Equals", needsValue: true },
|
|
3565
|
+
{ id: "less_than", label: "Less Than", needsValue: true },
|
|
3566
|
+
{ id: "less_than_equals", label: "Less Than or Equals", needsValue: true },
|
|
3567
|
+
{ id: "like", label: "Like", needsValue: true },
|
|
3568
|
+
{ id: "contains", label: "Contains", needsValue: true },
|
|
3569
|
+
{ id: "starts_with", label: "Starts With", needsValue: true },
|
|
3570
|
+
{ id: "ends_with", label: "Ends With", needsValue: true },
|
|
3571
|
+
{ id: "is_null", label: "Is Null", needsValue: false },
|
|
3572
|
+
{ id: "is_not_null", label: "Is Not Null", needsValue: false },
|
|
3573
|
+
{ id: "is_empty", label: "Is Empty", needsValue: false }
|
|
3574
|
+
];
|
|
3575
|
+
function isMissingValue(value) {
|
|
3576
|
+
return value === undefined || value === null || value === "";
|
|
4052
3577
|
}
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
3578
|
+
var ConditionConfig = {
|
|
3579
|
+
defaults: {
|
|
3580
|
+
mode: "visual",
|
|
3581
|
+
filters: [],
|
|
3582
|
+
condition: ""
|
|
3583
|
+
},
|
|
3584
|
+
schema: {
|
|
3585
|
+
mode: { type: "select", required: true, options: ["visual", "expression"] },
|
|
3586
|
+
filters: { type: "array", required: false },
|
|
3587
|
+
condition: { type: "expression", required: false }
|
|
3588
|
+
},
|
|
3589
|
+
validate: (config) => {
|
|
3590
|
+
const errors = [];
|
|
3591
|
+
if (config.mode === "expression") {
|
|
3592
|
+
if (!config.condition) {
|
|
3593
|
+
errors.push("Condition expression is required");
|
|
3594
|
+
} else {
|
|
3595
|
+
try {
|
|
3596
|
+
new Function(`return ${config.condition}`);
|
|
3597
|
+
} catch (e) {
|
|
3598
|
+
errors.push(`Invalid expression: ${e instanceof Error ? e.message : "Unknown error"}`);
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
} else {
|
|
3602
|
+
if (!config.filters || config.filters.length === 0) {
|
|
3603
|
+
errors.push("At least one condition filter is required");
|
|
3604
|
+
} else {
|
|
3605
|
+
config.filters.forEach((f, i) => {
|
|
3606
|
+
if (!f.field)
|
|
3607
|
+
errors.push(`Filter ${i + 1}: Field is required`);
|
|
3608
|
+
if (!f.operator)
|
|
3609
|
+
errors.push(`Filter ${i + 1}: Operator is required`);
|
|
3610
|
+
const opDef = CONDITION_OPERATORS.find((o) => o.id === f.operator);
|
|
3611
|
+
if (opDef?.needsValue && isMissingValue(f.value)) {
|
|
3612
|
+
errors.push(`Filter ${i + 1}: Value is required`);
|
|
3613
|
+
}
|
|
3614
|
+
});
|
|
3615
|
+
}
|
|
4064
3616
|
}
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
3617
|
+
return { valid: errors.length === 0, errors };
|
|
3618
|
+
},
|
|
3619
|
+
merge: (userConfig) => {
|
|
3620
|
+
return { ...ConditionConfig.defaults, ...userConfig };
|
|
3621
|
+
},
|
|
3622
|
+
generateName: (filters) => {
|
|
3623
|
+
if (filters.length === 0)
|
|
3624
|
+
return "If …";
|
|
3625
|
+
const parts = filters.map((f, i) => {
|
|
3626
|
+
const opLabel = CONDITION_OPERATORS.find((o) => o.id === f.operator)?.label || f.operator;
|
|
3627
|
+
const base = `${f.field || "?"} ${opLabel}${f.value ? ` ${f.value}` : ""}`;
|
|
3628
|
+
if (i === 0)
|
|
3629
|
+
return base;
|
|
3630
|
+
return `${(f.logicalOperator || "and").toUpperCase()} ${base}`;
|
|
3631
|
+
});
|
|
3632
|
+
return `If ${parts.join(" ")}`;
|
|
3633
|
+
},
|
|
3634
|
+
getLabel: (config) => {
|
|
3635
|
+
if (config.mode === "expression")
|
|
3636
|
+
return `If ${config.condition || "…"}`;
|
|
3637
|
+
return ConditionConfig.generateName(config.filters);
|
|
3638
|
+
},
|
|
3639
|
+
createFilterId: () => `cond_${Date.now()}_${randomSuffix(5)}`
|
|
3640
|
+
};
|
|
3641
|
+
|
|
3642
|
+
// src/catalog/logic/for-each/config.ts
|
|
3643
|
+
var SOURCELESS_MODES = new Set(["range", "cursor", "while"]);
|
|
3644
|
+
var ForEachConfig = {
|
|
3645
|
+
defaults: {
|
|
3646
|
+
arrayPath: "",
|
|
3647
|
+
iterationMode: "records",
|
|
3648
|
+
itemVariable: "item",
|
|
3649
|
+
indexVariable: "index",
|
|
3650
|
+
batchSize: 0,
|
|
3651
|
+
continueOnError: false,
|
|
3652
|
+
concurrency: "sequential",
|
|
3653
|
+
collect: "array"
|
|
3654
|
+
},
|
|
3655
|
+
schema: {
|
|
3656
|
+
arrayPath: {
|
|
3657
|
+
type: "select",
|
|
3658
|
+
required: false,
|
|
3659
|
+
dynamic: true,
|
|
3660
|
+
description: "A path to an array in the upstream step's output (the Query's object name, or a field holding a list) — the values come from that step's data, not from the catalog"
|
|
3661
|
+
},
|
|
3662
|
+
iterationMode: {
|
|
3663
|
+
type: "select",
|
|
3664
|
+
required: false,
|
|
3665
|
+
options: ["records", "fields", "range", "nested", "batch", "cursor", "while", "array"],
|
|
3666
|
+
description: "What one iteration is: records (one per record, the default; array is its legacy alias), fields, range, nested, batch, cursor or while"
|
|
3667
|
+
},
|
|
3668
|
+
itemVariable: { type: "string", required: true },
|
|
3669
|
+
indexVariable: { type: "string", required: false },
|
|
3670
|
+
batchSize: { type: "number", required: false },
|
|
3671
|
+
continueOnError: { type: "boolean", required: false },
|
|
3672
|
+
collect: {
|
|
3673
|
+
type: "select",
|
|
3674
|
+
required: false,
|
|
3675
|
+
options: ["array", "none", "groupBy", "reduce", "rows"],
|
|
3676
|
+
description: "What the Done port carries besides the counts: array (each iteration's result in `results`, the default), none, groupBy (by groupByPath), " + "reduce (reduceOp over reducePath), or rows — also `data`: every row the body produced (each iteration's Query/Transform rows, else its result), " + "so ONE step on the Done port maps and writes them all instead of a write per iteration."
|
|
4068
3677
|
}
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
return;
|
|
4087
|
-
const path = (arrayPath || "").trim();
|
|
4088
|
-
const isRoot = isRootPath(path);
|
|
4089
|
-
const schema = node.outputSchema || node.config?._outputSchema;
|
|
4090
|
-
if (schema?.properties) {
|
|
4091
|
-
if (isRoot) {
|
|
4092
|
-
const hasDeepFields = Object.values(schema.properties).some((f) => f?.properties || f?.items);
|
|
4093
|
-
if (hasDeepFields)
|
|
4094
|
-
return schema;
|
|
4095
|
-
} else {
|
|
4096
|
-
const resolved = resolveSchemaPath(schema, path);
|
|
4097
|
-
if (resolved)
|
|
4098
|
-
return resolved;
|
|
3678
|
+
},
|
|
3679
|
+
validate: (config) => {
|
|
3680
|
+
const errors = [];
|
|
3681
|
+
const mode = config.iterationMode || "records";
|
|
3682
|
+
if (mode === "range") {
|
|
3683
|
+
if (!config.count || config.count <= 0) {
|
|
3684
|
+
errors.push("Range mode needs a positive iteration count");
|
|
3685
|
+
}
|
|
3686
|
+
} else if (mode === "nested") {
|
|
3687
|
+
if (!config.arrayPath)
|
|
3688
|
+
errors.push("Select the records to drill into");
|
|
3689
|
+
if (!config.nestedPath)
|
|
3690
|
+
errors.push('Nested mode needs the sub-array path to iterate (e.g. "lineItems")');
|
|
3691
|
+
} else if (!SOURCELESS_MODES.has(mode)) {
|
|
3692
|
+
if (!config.arrayPath) {
|
|
3693
|
+
errors.push("Array path is required — select which records to iterate over");
|
|
3694
|
+
}
|
|
4099
3695
|
}
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
if (records && records.length > 0 && typeof records[0] === "object" && records[0] !== null) {
|
|
4103
|
-
const sample = records[0];
|
|
4104
|
-
if (isRoot)
|
|
4105
|
-
return inferSchemaFromSample(sample);
|
|
4106
|
-
const resolved = resolveSamplePath(sample, path);
|
|
4107
|
-
if (Array.isArray(resolved) && resolved.length > 0 && typeof resolved[0] === "object" && resolved[0] !== null) {
|
|
4108
|
-
return inferSchemaFromSample(resolved[0]);
|
|
3696
|
+
if (config.concurrency === "parallel" && config.maxConcurrency !== undefined && config.maxConcurrency < 1) {
|
|
3697
|
+
errors.push("Max concurrency must be at least 1");
|
|
4109
3698
|
}
|
|
4110
|
-
if (
|
|
4111
|
-
|
|
3699
|
+
if (config.batchSize !== undefined && config.batchSize < 0) {
|
|
3700
|
+
errors.push("Batch size must be 0 (all) or a positive number");
|
|
4112
3701
|
}
|
|
4113
|
-
if (
|
|
4114
|
-
|
|
3702
|
+
if (config.collect === "groupBy" && !config.groupByPath) {
|
|
3703
|
+
errors.push("Group-by collection needs a field path to group on");
|
|
3704
|
+
}
|
|
3705
|
+
return { valid: errors.length === 0, errors };
|
|
3706
|
+
},
|
|
3707
|
+
merge: (userConfig) => {
|
|
3708
|
+
return { ...ForEachConfig.defaults, ...userConfig };
|
|
3709
|
+
},
|
|
3710
|
+
getLabel: (config) => {
|
|
3711
|
+
const mode = config.iterationMode || "records";
|
|
3712
|
+
if (mode === "range")
|
|
3713
|
+
return `ForEach ×${config.count ?? "…"}`;
|
|
3714
|
+
if (mode === "cursor")
|
|
3715
|
+
return "ForEach (paginate)";
|
|
3716
|
+
if (mode === "while")
|
|
3717
|
+
return "ForEach (while)";
|
|
3718
|
+
return `ForEach in ${config.arrayPath || "…"}`;
|
|
3719
|
+
},
|
|
3720
|
+
generateName: (arrayPath) => {
|
|
3721
|
+
return `ForEach in ${arrayPath || "…"}`;
|
|
4115
3722
|
}
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
}
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
3723
|
+
};
|
|
3724
|
+
|
|
3725
|
+
// src/catalog/triggers/schedule-trigger/config.ts
|
|
3726
|
+
var ScheduleTriggerConfig = {
|
|
3727
|
+
defaults: {
|
|
3728
|
+
scheduleType: "interval",
|
|
3729
|
+
interval: 60,
|
|
3730
|
+
intervalUnit: "minutes",
|
|
3731
|
+
timezone: "UTC"
|
|
3732
|
+
},
|
|
3733
|
+
schema: {
|
|
3734
|
+
scheduleType: { type: "select", required: true, options: ["interval", "cron", "specific"] },
|
|
3735
|
+
interval: { type: "number", required: false, min: 1 },
|
|
3736
|
+
intervalUnit: { type: "select", required: false, options: ["minutes", "hours", "days", "weeks"] },
|
|
3737
|
+
cronExpression: { type: "string", required: false },
|
|
3738
|
+
timezone: { type: "string", required: false }
|
|
3739
|
+
},
|
|
3740
|
+
validate: (config) => {
|
|
3741
|
+
const errors = [];
|
|
3742
|
+
if (!config.scheduleType) {
|
|
3743
|
+
errors.push("Schedule type is required");
|
|
3744
|
+
}
|
|
3745
|
+
if (config.scheduleType === "interval") {
|
|
3746
|
+
if (!config.interval || config.interval < 1) {
|
|
3747
|
+
errors.push("Interval must be at least 1");
|
|
4136
3748
|
}
|
|
4137
|
-
const at = resolveSamplePath(r, path);
|
|
4138
|
-
if (Array.isArray(at))
|
|
4139
|
-
for (const item of at.slice(0, 25))
|
|
4140
|
-
addKeys(item);
|
|
4141
|
-
else if (at && typeof at === "object")
|
|
4142
|
-
addKeys(at);
|
|
4143
|
-
else if (!path.includes("."))
|
|
4144
|
-
addKeys(r);
|
|
4145
3749
|
}
|
|
3750
|
+
if (config.scheduleType === "cron" && !config.cronExpression) {
|
|
3751
|
+
errors.push("Cron expression is required");
|
|
3752
|
+
}
|
|
3753
|
+
return { valid: errors.length === 0, errors };
|
|
3754
|
+
},
|
|
3755
|
+
merge: (userConfig) => {
|
|
3756
|
+
return { ...ScheduleTriggerConfig.defaults, ...userConfig };
|
|
3757
|
+
},
|
|
3758
|
+
getLabel: (config) => {
|
|
3759
|
+
if (config.scheduleType === "interval") {
|
|
3760
|
+
return `Every ${config.interval} ${config.intervalUnit}`;
|
|
3761
|
+
}
|
|
3762
|
+
return `Schedule: ${config.scheduleType}`;
|
|
4146
3763
|
}
|
|
4147
|
-
return Array.from(set);
|
|
4148
|
-
}
|
|
4149
|
-
// src/catalog/storage/storageCapabilities.ts
|
|
4150
|
-
var contentTypeField = {
|
|
4151
|
-
key: "contentType",
|
|
4152
|
-
label: "Content type",
|
|
4153
|
-
type: "string",
|
|
4154
|
-
placeholder: "application/octet-stream",
|
|
4155
|
-
advanced: true,
|
|
4156
|
-
hint: "MIME type for the uploaded file."
|
|
4157
3764
|
};
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
{ value: "list", label: "List files", path: { key: "prefix", label: "Folder", selectMode: "folder" }, fields: [{ key: "maxKeys", label: "Max results", type: "number", defaultValue: 100, advanced: true }] },
|
|
4165
|
-
{ value: "download", label: "Download file", path: { key: "key", label: "File", selectMode: "both", required: true } },
|
|
4166
|
-
{ value: "upload", label: "Upload file", path: { key: "key", label: "Destination key", selectMode: "both", required: true }, content: true, fields: [contentTypeField] },
|
|
4167
|
-
{ value: "delete", label: "Delete file", path: { key: "key", label: "File", selectMode: "both", required: true } },
|
|
4168
|
-
{ value: "copy", label: "Copy file", path: { key: "key", label: "Source file", selectMode: "both", required: true }, dest: { key: "destKey", label: "Destination key", selectMode: "both", required: true } },
|
|
4169
|
-
{ value: "move", label: "Move / rename", path: { key: "key", label: "Source file", selectMode: "both", required: true }, dest: { key: "destKey", label: "Destination key", selectMode: "both", required: true } },
|
|
4170
|
-
{ value: "presign", label: "Get signed URL", path: { key: "key", label: "File", selectMode: "both", required: true }, fields: [{ key: "expiresIn", label: "Expiry (seconds)", type: "number", defaultValue: 3600, advanced: true }] }
|
|
4171
|
-
]
|
|
3765
|
+
|
|
3766
|
+
// src/catalog/triggers/webhook-trigger/config.ts
|
|
3767
|
+
var WebhookTriggerConfig = {
|
|
3768
|
+
defaults: {
|
|
3769
|
+
method: "POST",
|
|
3770
|
+
requireAuth: true
|
|
4172
3771
|
},
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
operations: [
|
|
4178
|
-
{ value: "list", label: "List files", path: { key: "remotePath", label: "Folder", selectMode: "folder" } },
|
|
4179
|
-
{ value: "download", label: "Download file", path: { key: "remotePath", label: "File", selectMode: "both", required: true } },
|
|
4180
|
-
{ value: "upload", label: "Upload file", path: { key: "remotePath", label: "Destination path", selectMode: "both", required: true }, content: true },
|
|
4181
|
-
{ value: "delete", label: "Delete file", path: { key: "remotePath", label: "File", selectMode: "both", required: true } },
|
|
4182
|
-
{ value: "move", label: "Move / rename", path: { key: "remotePath", label: "Source file", selectMode: "both", required: true }, dest: { key: "destPath", label: "Destination path", selectMode: "both", required: true } }
|
|
4183
|
-
]
|
|
3772
|
+
schema: {
|
|
3773
|
+
webhookPath: { type: "string", required: false },
|
|
3774
|
+
method: { type: "select", required: true, options: ["GET", "POST", "PUT", "PATCH", "DELETE"] },
|
|
3775
|
+
requireAuth: { type: "boolean", required: false }
|
|
4184
3776
|
},
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
{ value: "download", label: "Download file", path: { key: "path", label: "File", selectMode: "both", required: true } },
|
|
4192
|
-
{ value: "upload", label: "Upload file", path: { key: "path", label: "Destination path", selectMode: "both", required: true }, content: true, fields: [{ key: "mode", label: "Write mode", type: "select", options: [{ label: "Overwrite", value: "overwrite" }, { label: "Add (keep both)", value: "add" }], defaultValue: "overwrite", advanced: true }] },
|
|
4193
|
-
{ value: "delete", label: "Delete file", path: { key: "path", label: "File", selectMode: "both", required: true } },
|
|
4194
|
-
{ value: "copy", label: "Copy file", path: { key: "path", label: "Source file", selectMode: "both", required: true }, dest: { key: "destPath", label: "Destination path", selectMode: "both", required: true } },
|
|
4195
|
-
{ value: "move", label: "Move / rename", path: { key: "path", label: "Source file", selectMode: "both", required: true }, dest: { key: "destPath", label: "Destination path", selectMode: "both", required: true } }
|
|
4196
|
-
]
|
|
3777
|
+
validate: (config) => {
|
|
3778
|
+
const errors = [];
|
|
3779
|
+
if (!config.method) {
|
|
3780
|
+
errors.push("HTTP method is required");
|
|
3781
|
+
}
|
|
3782
|
+
return { valid: errors.length === 0, errors };
|
|
4197
3783
|
},
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
label: "OneDrive",
|
|
4201
|
-
browse: true,
|
|
4202
|
-
operations: [
|
|
4203
|
-
{ value: "list", label: "List files", path: { key: "path", label: "Folder", selectMode: "folder" } },
|
|
4204
|
-
{ value: "download", label: "Download file", path: { key: "path", label: "File", selectMode: "both", required: true } },
|
|
4205
|
-
{ value: "upload", label: "Upload file", path: { key: "path", label: "Destination path", selectMode: "both", required: true }, content: true, fields: [contentTypeField] },
|
|
4206
|
-
{ value: "delete", label: "Delete file", path: { key: "path", label: "File", selectMode: "both", required: true } },
|
|
4207
|
-
{ value: "copy", label: "Copy file", path: { key: "path", label: "Source file", selectMode: "both", required: true }, dest: { key: "destPath", label: "Destination path", selectMode: "both", required: true } },
|
|
4208
|
-
{ value: "move", label: "Move / rename", path: { key: "path", label: "Source file", selectMode: "both", required: true }, dest: { key: "destPath", label: "Destination path", selectMode: "both", required: true } }
|
|
4209
|
-
]
|
|
3784
|
+
merge: (userConfig) => {
|
|
3785
|
+
return { ...WebhookTriggerConfig.defaults, ...userConfig };
|
|
4210
3786
|
},
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
3787
|
+
getLabel: (config) => {
|
|
3788
|
+
return `Webhook ${config.method}${config.webhookPath ? ` ${config.webhookPath}` : ""}`;
|
|
3789
|
+
}
|
|
3790
|
+
};
|
|
3791
|
+
|
|
3792
|
+
// src/catalog/utility/debug/config.ts
|
|
3793
|
+
var DebugConfig = {
|
|
3794
|
+
schema: {
|
|
3795
|
+
mode: { type: "select", required: true, options: ["log", "snapshot", "watch", "assert", "profile"] },
|
|
3796
|
+
label: { type: "string", required: false },
|
|
3797
|
+
pinned: { type: "boolean", required: false },
|
|
3798
|
+
condition: { type: "expression", required: false },
|
|
3799
|
+
sampling: { type: "object", required: false },
|
|
3800
|
+
log: { type: "object", required: false },
|
|
3801
|
+
snapshot: { type: "object", required: false },
|
|
3802
|
+
watch: { type: "object", required: false },
|
|
3803
|
+
assert: { type: "object", required: false },
|
|
3804
|
+
profile: { type: "object", required: false }
|
|
3805
|
+
},
|
|
3806
|
+
defaults: {
|
|
3807
|
+
mode: "log",
|
|
3808
|
+
label: "",
|
|
3809
|
+
log: { level: "info", message: "" }
|
|
3810
|
+
},
|
|
3811
|
+
validate(config) {
|
|
3812
|
+
const errors = [];
|
|
3813
|
+
if (!config.mode)
|
|
3814
|
+
errors.push("Mode is required.");
|
|
3815
|
+
if (!config.label?.trim())
|
|
3816
|
+
errors.push("Label is required.");
|
|
3817
|
+
switch (config.mode) {
|
|
3818
|
+
case "log":
|
|
3819
|
+
if (!config.log?.message?.trim())
|
|
3820
|
+
errors.push("Log mode: message is required.");
|
|
3821
|
+
if (!config.log?.level)
|
|
3822
|
+
errors.push("Log mode: level is required.");
|
|
3823
|
+
break;
|
|
3824
|
+
case "snapshot":
|
|
3825
|
+
break;
|
|
3826
|
+
case "watch":
|
|
3827
|
+
if (!config.watch?.expressions?.length) {
|
|
3828
|
+
errors.push("Watch mode: add at least one expression.");
|
|
3829
|
+
} else {
|
|
3830
|
+
for (const e of config.watch.expressions) {
|
|
3831
|
+
if (!e.label?.trim())
|
|
3832
|
+
errors.push("Watch mode: each expression needs a label.");
|
|
3833
|
+
if (!e.expression?.trim())
|
|
3834
|
+
errors.push("Watch mode: each expression needs an expression string.");
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
break;
|
|
3838
|
+
case "assert":
|
|
3839
|
+
if (!config.assert?.predicate?.trim())
|
|
3840
|
+
errors.push("Assert mode: predicate is required.");
|
|
3841
|
+
if (!config.assert?.failureMessage?.trim())
|
|
3842
|
+
errors.push("Assert mode: failure message is required.");
|
|
3843
|
+
break;
|
|
3844
|
+
case "profile":
|
|
3845
|
+
if (!config.profile?.scope)
|
|
3846
|
+
errors.push("Profile mode: scope is required.");
|
|
3847
|
+
break;
|
|
3848
|
+
}
|
|
3849
|
+
return { valid: errors.length === 0, errors };
|
|
4236
3850
|
},
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
3851
|
+
migrateLegacyStub(config) {
|
|
3852
|
+
if (config.mode) {
|
|
3853
|
+
const { value: _stub, ...rest } = config;
|
|
3854
|
+
return { ...DebugConfig.defaults, ...rest };
|
|
3855
|
+
}
|
|
3856
|
+
return {
|
|
3857
|
+
mode: "log",
|
|
3858
|
+
label: config.label ?? "Debug",
|
|
3859
|
+
log: {
|
|
3860
|
+
level: "info",
|
|
3861
|
+
message: config.label ?? "Debug",
|
|
3862
|
+
includeInputData: true
|
|
3863
|
+
}
|
|
3864
|
+
};
|
|
4250
3865
|
},
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
{ value: "list", label: "List files", path: { key: "prefix", label: "Folder", selectMode: "folder" }, fields: [{ key: "maxKeys", label: "Max results", type: "number", defaultValue: 100, advanced: true }] },
|
|
4257
|
-
{ value: "download", label: "Download file", path: { key: "blob", label: "File", selectMode: "both", required: true } },
|
|
4258
|
-
{ value: "upload", label: "Upload file", path: { key: "blob", label: "Destination blob", selectMode: "both", required: true }, content: true, fields: [contentTypeField] },
|
|
4259
|
-
{ value: "delete", label: "Delete file", path: { key: "blob", label: "File", selectMode: "both", required: true } },
|
|
4260
|
-
{ value: "copy", label: "Copy file", path: { key: "blob", label: "Source blob", selectMode: "both", required: true }, dest: { key: "destBlob", label: "Destination blob", selectMode: "both", required: true } },
|
|
4261
|
-
{ value: "move", label: "Move / rename", path: { key: "blob", label: "Source blob", selectMode: "both", required: true }, dest: { key: "destBlob", label: "Destination blob", selectMode: "both", required: true } },
|
|
4262
|
-
{ value: "presign", label: "Get signed URL", path: { key: "blob", label: "File", selectMode: "both", required: true }, fields: [{ key: "expiresIn", label: "Expiry (seconds)", type: "number", defaultValue: 3600, advanced: true }] }
|
|
4263
|
-
]
|
|
3866
|
+
merge(userConfig) {
|
|
3867
|
+
return { ...DebugConfig.defaults, ...userConfig };
|
|
3868
|
+
},
|
|
3869
|
+
getLabel(config) {
|
|
3870
|
+
return `Debug [${config.mode}]: ${config.label}`;
|
|
4264
3871
|
}
|
|
4265
3872
|
};
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
{
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
3873
|
+
|
|
3874
|
+
// src/catalog/utility/variables/config.ts
|
|
3875
|
+
var RESERVED_KEYS2 = new Set(["_lineage", "_meta", "_redacted", "_error"]);
|
|
3876
|
+
var KEY_PATTERN2 = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
3877
|
+
var VariablesConfig = {
|
|
3878
|
+
schema: {
|
|
3879
|
+
fields: { type: "array", required: true }
|
|
3880
|
+
},
|
|
3881
|
+
defaults: {
|
|
3882
|
+
fields: []
|
|
3883
|
+
},
|
|
3884
|
+
validate(config) {
|
|
3885
|
+
const errors = [];
|
|
3886
|
+
const fields = config.fields ?? [];
|
|
3887
|
+
const seen = new Set;
|
|
3888
|
+
for (const f of fields) {
|
|
3889
|
+
if (!f.key) {
|
|
3890
|
+
errors.push("Each field needs a key.");
|
|
3891
|
+
continue;
|
|
3892
|
+
}
|
|
3893
|
+
if (RESERVED_KEYS2.has(f.key)) {
|
|
3894
|
+
errors.push(`"${f.key}" is reserved — pick another key.`);
|
|
3895
|
+
continue;
|
|
3896
|
+
}
|
|
3897
|
+
if (!KEY_PATTERN2.test(f.key)) {
|
|
3898
|
+
errors.push(`"${f.key}" is not a valid identifier (letters, digits, underscore; must not start with a digit).`);
|
|
3899
|
+
continue;
|
|
3900
|
+
}
|
|
3901
|
+
if (seen.has(f.key)) {
|
|
3902
|
+
errors.push(`Duplicate key "${f.key}".`);
|
|
3903
|
+
continue;
|
|
3904
|
+
}
|
|
3905
|
+
seen.add(f.key);
|
|
3906
|
+
if (!f.type)
|
|
3907
|
+
errors.push(`"${f.key}" is missing a type.`);
|
|
3908
|
+
if (!f.source)
|
|
3909
|
+
errors.push(`"${f.key}" is missing a source.`);
|
|
4302
3910
|
}
|
|
4303
|
-
|
|
4304
|
-
}
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
function getStorageProvider(connectorId) {
|
|
4310
|
-
if (!connectorId)
|
|
4311
|
-
return;
|
|
4312
|
-
return STORAGE_PROVIDERS[connectorId] ?? STORAGE_PROVIDERS[connectorId.replace(/-/g, "_")];
|
|
4313
|
-
}
|
|
4314
|
-
function storageConfigKeys() {
|
|
4315
|
-
const keys = new Set([
|
|
4316
|
-
"operation",
|
|
4317
|
-
"connector",
|
|
4318
|
-
"content",
|
|
4319
|
-
"outputAs",
|
|
4320
|
-
"path",
|
|
4321
|
-
"destPath",
|
|
4322
|
-
"destConnector",
|
|
4323
|
-
"sourceIsDirectory",
|
|
4324
|
-
"cursor",
|
|
4325
|
-
"fileBudget",
|
|
4326
|
-
"timeBudgetMs"
|
|
4327
|
-
]);
|
|
4328
|
-
for (const provider of Object.values(STORAGE_PROVIDERS)) {
|
|
4329
|
-
for (const op of provider.operations) {
|
|
4330
|
-
if (op.path)
|
|
4331
|
-
keys.add(op.path.key);
|
|
4332
|
-
if (op.dest)
|
|
4333
|
-
keys.add(op.dest.key);
|
|
4334
|
-
for (const f of op.fields ?? [])
|
|
4335
|
-
keys.add(f.key);
|
|
3911
|
+
return { valid: errors.length === 0, errors };
|
|
3912
|
+
},
|
|
3913
|
+
computeOutputSchema(fields) {
|
|
3914
|
+
const properties = {};
|
|
3915
|
+
for (const f of fields) {
|
|
3916
|
+
properties[f.key] = fieldToJsonSchema(f);
|
|
4336
3917
|
}
|
|
3918
|
+
return { type: "object", properties };
|
|
4337
3919
|
}
|
|
4338
|
-
|
|
3920
|
+
};
|
|
3921
|
+
function fieldToJsonSchema(f) {
|
|
3922
|
+
const node = { type: f.type };
|
|
3923
|
+
if (f.description)
|
|
3924
|
+
node.description = f.description;
|
|
3925
|
+
if (f.validation?.enum)
|
|
3926
|
+
node.enum = f.validation.enum;
|
|
3927
|
+
return node;
|
|
4339
3928
|
}
|
|
3929
|
+
|
|
4340
3930
|
// src/catalog/ai/ai-agent/config.ts
|
|
4341
3931
|
var AiAgentConfig = {
|
|
4342
3932
|
defaults: {
|
|
@@ -4368,6 +3958,7 @@ var AiAgentConfig = {
|
|
|
4368
3958
|
merge: (userConfig) => ({ ...AiAgentConfig.defaults, ...userConfig }),
|
|
4369
3959
|
getLabel: (config) => config.agentId ? `Agent ${config.agentId}` : "AI Agent"
|
|
4370
3960
|
};
|
|
3961
|
+
|
|
4371
3962
|
// src/catalog/ai/idp-extract/config.ts
|
|
4372
3963
|
var IDP_EXTRACT_STRATEGIES = ["vision-direct", "auto"];
|
|
4373
3964
|
var IdpExtractConfig = {
|
|
@@ -4401,6 +3992,7 @@ var IdpExtractConfig = {
|
|
|
4401
3992
|
merge: (userConfig) => ({ ...IdpExtractConfig.defaults, ...userConfig }),
|
|
4402
3993
|
getLabel: (config) => config.model ? `IDP: ${config.model}` : "IDP Extract"
|
|
4403
3994
|
};
|
|
3995
|
+
|
|
4404
3996
|
// src/catalog/logic/group/config.ts
|
|
4405
3997
|
var GroupConfig = {
|
|
4406
3998
|
defaults: {},
|
|
@@ -4409,6 +4001,7 @@ var GroupConfig = {
|
|
|
4409
4001
|
merge: (_userConfig) => ({}),
|
|
4410
4002
|
getLabel: () => "Group"
|
|
4411
4003
|
};
|
|
4004
|
+
|
|
4412
4005
|
// src/catalog/triggers/connector-trigger/events.ts
|
|
4413
4006
|
var EVENT_CAPABLE_CONNECTOR_IDS = new Set([
|
|
4414
4007
|
"quickbooks_online",
|
|
@@ -4575,6 +4168,7 @@ var ConnectorTriggerConfig = {
|
|
|
4575
4168
|
merge: (userConfig) => ({ ...ConnectorTriggerConfig.defaults, ...userConfig }),
|
|
4576
4169
|
getLabel: (config) => config.summary || config.connectorName || "Connector Trigger"
|
|
4577
4170
|
};
|
|
4171
|
+
|
|
4578
4172
|
// src/catalog/storage/fileEditContract.ts
|
|
4579
4173
|
var FILE_FORMAT_OPERATIONS = ["parse", "generate", "edit"];
|
|
4580
4174
|
var FILE_EDIT_OPERATIONS = ["append", "update", "upsert", "set_fields"];
|
|
@@ -4613,6 +4207,7 @@ function fileFormatErrors(config) {
|
|
|
4613
4207
|
}
|
|
4614
4208
|
return [...errors, ...fileEditErrors(config)];
|
|
4615
4209
|
}
|
|
4210
|
+
|
|
4616
4211
|
// src/catalog/storage/csv-action/config.ts
|
|
4617
4212
|
var CsvActionConfig = {
|
|
4618
4213
|
defaults: {
|
|
@@ -4639,6 +4234,7 @@ var CsvActionConfig = {
|
|
|
4639
4234
|
merge: (userConfig) => ({ ...CsvActionConfig.defaults, ...userConfig }),
|
|
4640
4235
|
getLabel: (config) => `CSV: ${config.operation || "parse"}`
|
|
4641
4236
|
};
|
|
4237
|
+
|
|
4642
4238
|
// src/catalog/storage/json-action/config.ts
|
|
4643
4239
|
var JsonActionConfig = {
|
|
4644
4240
|
defaults: {
|
|
@@ -4667,6 +4263,7 @@ var JsonActionConfig = {
|
|
|
4667
4263
|
merge: (userConfig) => ({ ...JsonActionConfig.defaults, ...userConfig }),
|
|
4668
4264
|
getLabel: (config) => `JSON: ${config.operation || "parse"}`
|
|
4669
4265
|
};
|
|
4266
|
+
|
|
4670
4267
|
// src/catalog/storage/xml-action/config.ts
|
|
4671
4268
|
var XmlActionConfig = {
|
|
4672
4269
|
defaults: {
|
|
@@ -4687,31 +4284,225 @@ var XmlActionConfig = {
|
|
|
4687
4284
|
const errors = fileFormatErrors(config);
|
|
4688
4285
|
return { valid: errors.length === 0, errors };
|
|
4689
4286
|
},
|
|
4690
|
-
merge: (userConfig) => ({ ...XmlActionConfig.defaults, ...userConfig }),
|
|
4691
|
-
getLabel: (config) => `XML: ${config.operation || "parse"}`
|
|
4692
|
-
};
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4287
|
+
merge: (userConfig) => ({ ...XmlActionConfig.defaults, ...userConfig }),
|
|
4288
|
+
getLabel: (config) => `XML: ${config.operation || "parse"}`
|
|
4289
|
+
};
|
|
4290
|
+
|
|
4291
|
+
// src/catalog/storage/excel-action/config.ts
|
|
4292
|
+
var ExcelActionConfig = {
|
|
4293
|
+
defaults: {
|
|
4294
|
+
operation: "parse",
|
|
4295
|
+
headerRow: 1,
|
|
4296
|
+
fileName: "export.xlsx"
|
|
4297
|
+
},
|
|
4298
|
+
schema: {
|
|
4299
|
+
operation: { type: "select", required: true, options: FILE_FORMAT_OPERATIONS, description: "parse (Excel → records), generate (records → Excel) or edit an existing file" },
|
|
4300
|
+
content: { type: "text", required: false, description: "A reference to an earlier step's Excel file (its s3Key output), or base64 bytes" },
|
|
4301
|
+
sheetName: { type: "string", required: false, description: "Sheet to read; the first sheet when absent" },
|
|
4302
|
+
headerRow: { type: "number", required: false, min: 1, description: "1-based row that holds the column headers" },
|
|
4303
|
+
fileName: { type: "string", required: false, description: "generate: name of the produced file" },
|
|
4304
|
+
...FILE_EDIT_SCHEMA
|
|
4305
|
+
},
|
|
4306
|
+
validate: (config) => {
|
|
4307
|
+
const errors = fileFormatErrors(config);
|
|
4308
|
+
return { valid: errors.length === 0, errors };
|
|
4309
|
+
},
|
|
4310
|
+
merge: (userConfig) => ({ ...ExcelActionConfig.defaults, ...userConfig }),
|
|
4311
|
+
getLabel: (config) => `Excel: ${config.operation || "parse"}`
|
|
4312
|
+
};
|
|
4313
|
+
|
|
4314
|
+
// src/catalog/storage/storageCapabilities.ts
|
|
4315
|
+
var contentTypeField = {
|
|
4316
|
+
key: "contentType",
|
|
4317
|
+
label: "Content type",
|
|
4318
|
+
type: "string",
|
|
4319
|
+
placeholder: "application/octet-stream",
|
|
4320
|
+
advanced: true,
|
|
4321
|
+
hint: "MIME type for the uploaded file."
|
|
4322
|
+
};
|
|
4323
|
+
var STORAGE_PROVIDERS = {
|
|
4324
|
+
aws_s3: {
|
|
4325
|
+
connectorId: "aws_s3",
|
|
4326
|
+
label: "Amazon S3",
|
|
4327
|
+
browse: true,
|
|
4328
|
+
operations: [
|
|
4329
|
+
{ value: "list", label: "List files", path: { key: "prefix", label: "Folder", selectMode: "folder" }, fields: [{ key: "maxKeys", label: "Max results", type: "number", defaultValue: 100, advanced: true }] },
|
|
4330
|
+
{ value: "download", label: "Download file", path: { key: "key", label: "File", selectMode: "both", required: true } },
|
|
4331
|
+
{ value: "upload", label: "Upload file", path: { key: "key", label: "Destination key", selectMode: "both", required: true }, content: true, fields: [contentTypeField] },
|
|
4332
|
+
{ value: "delete", label: "Delete file", path: { key: "key", label: "File", selectMode: "both", required: true } },
|
|
4333
|
+
{ value: "copy", label: "Copy file", path: { key: "key", label: "Source file", selectMode: "both", required: true }, dest: { key: "destKey", label: "Destination key", selectMode: "both", required: true } },
|
|
4334
|
+
{ value: "move", label: "Move / rename", path: { key: "key", label: "Source file", selectMode: "both", required: true }, dest: { key: "destKey", label: "Destination key", selectMode: "both", required: true } },
|
|
4335
|
+
{ value: "presign", label: "Get signed URL", path: { key: "key", label: "File", selectMode: "both", required: true }, fields: [{ key: "expiresIn", label: "Expiry (seconds)", type: "number", defaultValue: 3600, advanced: true }] }
|
|
4336
|
+
]
|
|
4337
|
+
},
|
|
4338
|
+
ftp_sftp: {
|
|
4339
|
+
connectorId: "ftp_sftp",
|
|
4340
|
+
label: "FTP / SFTP",
|
|
4341
|
+
browse: true,
|
|
4342
|
+
operations: [
|
|
4343
|
+
{ value: "list", label: "List files", path: { key: "remotePath", label: "Folder", selectMode: "folder" } },
|
|
4344
|
+
{ value: "download", label: "Download file", path: { key: "remotePath", label: "File", selectMode: "both", required: true } },
|
|
4345
|
+
{ value: "upload", label: "Upload file", path: { key: "remotePath", label: "Destination path", selectMode: "both", required: true }, content: true },
|
|
4346
|
+
{ value: "delete", label: "Delete file", path: { key: "remotePath", label: "File", selectMode: "both", required: true } },
|
|
4347
|
+
{ value: "move", label: "Move / rename", path: { key: "remotePath", label: "Source file", selectMode: "both", required: true }, dest: { key: "destPath", label: "Destination path", selectMode: "both", required: true } }
|
|
4348
|
+
]
|
|
4349
|
+
},
|
|
4350
|
+
dropbox: {
|
|
4351
|
+
connectorId: "dropbox",
|
|
4352
|
+
label: "Dropbox",
|
|
4353
|
+
browse: true,
|
|
4354
|
+
operations: [
|
|
4355
|
+
{ value: "list", label: "List files", path: { key: "path", label: "Folder", selectMode: "folder" } },
|
|
4356
|
+
{ value: "download", label: "Download file", path: { key: "path", label: "File", selectMode: "both", required: true } },
|
|
4357
|
+
{ value: "upload", label: "Upload file", path: { key: "path", label: "Destination path", selectMode: "both", required: true }, content: true, fields: [{ key: "mode", label: "Write mode", type: "select", options: [{ label: "Overwrite", value: "overwrite" }, { label: "Add (keep both)", value: "add" }], defaultValue: "overwrite", advanced: true }] },
|
|
4358
|
+
{ value: "delete", label: "Delete file", path: { key: "path", label: "File", selectMode: "both", required: true } },
|
|
4359
|
+
{ value: "copy", label: "Copy file", path: { key: "path", label: "Source file", selectMode: "both", required: true }, dest: { key: "destPath", label: "Destination path", selectMode: "both", required: true } },
|
|
4360
|
+
{ value: "move", label: "Move / rename", path: { key: "path", label: "Source file", selectMode: "both", required: true }, dest: { key: "destPath", label: "Destination path", selectMode: "both", required: true } }
|
|
4361
|
+
]
|
|
4362
|
+
},
|
|
4363
|
+
onedrive: {
|
|
4364
|
+
connectorId: "onedrive",
|
|
4365
|
+
label: "OneDrive",
|
|
4366
|
+
browse: true,
|
|
4367
|
+
operations: [
|
|
4368
|
+
{ value: "list", label: "List files", path: { key: "path", label: "Folder", selectMode: "folder" } },
|
|
4369
|
+
{ value: "download", label: "Download file", path: { key: "path", label: "File", selectMode: "both", required: true } },
|
|
4370
|
+
{ value: "upload", label: "Upload file", path: { key: "path", label: "Destination path", selectMode: "both", required: true }, content: true, fields: [contentTypeField] },
|
|
4371
|
+
{ value: "delete", label: "Delete file", path: { key: "path", label: "File", selectMode: "both", required: true } },
|
|
4372
|
+
{ value: "copy", label: "Copy file", path: { key: "path", label: "Source file", selectMode: "both", required: true }, dest: { key: "destPath", label: "Destination path", selectMode: "both", required: true } },
|
|
4373
|
+
{ value: "move", label: "Move / rename", path: { key: "path", label: "Source file", selectMode: "both", required: true }, dest: { key: "destPath", label: "Destination path", selectMode: "both", required: true } }
|
|
4374
|
+
]
|
|
4699
4375
|
},
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4376
|
+
google_drive: {
|
|
4377
|
+
connectorId: "google_drive",
|
|
4378
|
+
label: "Google Drive",
|
|
4379
|
+
browse: true,
|
|
4380
|
+
operations: [
|
|
4381
|
+
{ value: "list", label: "List files", path: { key: "folderId", label: "Folder", selectMode: "folder" }, fields: [{ key: "query", label: "Search query", type: "string", advanced: true }, { key: "pageSize", label: "Max results", type: "number", defaultValue: 50, advanced: true }] },
|
|
4382
|
+
{ value: "download", label: "Download file", path: { key: "fileId", label: "File", selectMode: "both", required: true } },
|
|
4383
|
+
{
|
|
4384
|
+
value: "upload",
|
|
4385
|
+
label: "Upload file",
|
|
4386
|
+
path: { key: "folderId", label: "Destination folder", selectMode: "both" },
|
|
4387
|
+
content: true,
|
|
4388
|
+
fields: [
|
|
4389
|
+
{ key: "fileName", label: "File name", type: "string", required: true },
|
|
4390
|
+
{ key: "folderName", label: "Folder path", type: "string", advanced: true, placeholder: "Reports/2026", hint: "Folders under My Drive, separated by /. Missing ones are created on the first run. Ignored when a destination folder is picked." },
|
|
4391
|
+
{ key: "ifExists", label: "If the file already exists", type: "select", options: [{ label: "Replace it (Drive keeps the versions)", value: "overwrite" }, { label: "Skip it", value: "skip" }, { label: "Keep both (rename)", value: "rename" }], defaultValue: "overwrite", advanced: true },
|
|
4392
|
+
{ key: "mimeType", label: "Content type", type: "string", advanced: true }
|
|
4393
|
+
]
|
|
4394
|
+
},
|
|
4395
|
+
{ value: "delete", label: "Delete file", path: { key: "fileId", label: "File", selectMode: "both", required: true } },
|
|
4396
|
+
{ value: "move", label: "Move", path: { key: "fileId", label: "File", selectMode: "both", required: true }, dest: { key: "destFolderId", label: "Destination folder", selectMode: "both", required: true } },
|
|
4397
|
+
{ value: "copy", label: "Copy", path: { key: "fileId", label: "File", selectMode: "both", required: true }, dest: { key: "destFolderId", label: "Destination folder", selectMode: "both" }, fields: [{ key: "fileName", label: "New name", type: "string" }] },
|
|
4398
|
+
{ value: "share", label: "Share", path: { key: "fileId", label: "File", selectMode: "both", required: true }, fields: [{ key: "shareEmail", label: "Share with (email)", type: "string", required: true }, { key: "shareRole", label: "Role", type: "select", options: [{ label: "Viewer", value: "reader" }, { label: "Editor", value: "writer" }], defaultValue: "reader" }] },
|
|
4399
|
+
{ value: "create_folder", label: "Create folder", path: { key: "folderId", label: "Parent folder", selectMode: "both" }, fields: [{ key: "fileName", label: "Folder name", type: "string", required: true }] }
|
|
4400
|
+
]
|
|
4707
4401
|
},
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4402
|
+
gcs: {
|
|
4403
|
+
connectorId: "gcs",
|
|
4404
|
+
label: "Google Cloud Storage",
|
|
4405
|
+
browse: true,
|
|
4406
|
+
operations: [
|
|
4407
|
+
{ value: "list", label: "List files", path: { key: "prefix", label: "Folder", selectMode: "folder" }, fields: [{ key: "maxKeys", label: "Max results", type: "number", defaultValue: 100, advanced: true }] },
|
|
4408
|
+
{ value: "download", label: "Download file", path: { key: "key", label: "File", selectMode: "both", required: true } },
|
|
4409
|
+
{ value: "upload", label: "Upload file", path: { key: "key", label: "Destination key", selectMode: "both", required: true }, content: true, fields: [contentTypeField] },
|
|
4410
|
+
{ value: "delete", label: "Delete file", path: { key: "key", label: "File", selectMode: "both", required: true } },
|
|
4411
|
+
{ value: "copy", label: "Copy file", path: { key: "key", label: "Source file", selectMode: "both", required: true }, dest: { key: "destKey", label: "Destination key", selectMode: "both", required: true } },
|
|
4412
|
+
{ value: "move", label: "Move / rename", path: { key: "key", label: "Source file", selectMode: "both", required: true }, dest: { key: "destKey", label: "Destination key", selectMode: "both", required: true } },
|
|
4413
|
+
{ value: "presign", label: "Get signed URL", path: { key: "key", label: "File", selectMode: "both", required: true }, fields: [{ key: "expiresIn", label: "Expiry (seconds)", type: "number", defaultValue: 3600, advanced: true }] }
|
|
4414
|
+
]
|
|
4711
4415
|
},
|
|
4712
|
-
|
|
4713
|
-
|
|
4416
|
+
azure_blob: {
|
|
4417
|
+
connectorId: "azure_blob",
|
|
4418
|
+
label: "Azure Blob Storage",
|
|
4419
|
+
browse: true,
|
|
4420
|
+
operations: [
|
|
4421
|
+
{ value: "list", label: "List files", path: { key: "prefix", label: "Folder", selectMode: "folder" }, fields: [{ key: "maxKeys", label: "Max results", type: "number", defaultValue: 100, advanced: true }] },
|
|
4422
|
+
{ value: "download", label: "Download file", path: { key: "blob", label: "File", selectMode: "both", required: true } },
|
|
4423
|
+
{ value: "upload", label: "Upload file", path: { key: "blob", label: "Destination blob", selectMode: "both", required: true }, content: true, fields: [contentTypeField] },
|
|
4424
|
+
{ value: "delete", label: "Delete file", path: { key: "blob", label: "File", selectMode: "both", required: true } },
|
|
4425
|
+
{ value: "copy", label: "Copy file", path: { key: "blob", label: "Source blob", selectMode: "both", required: true }, dest: { key: "destBlob", label: "Destination blob", selectMode: "both", required: true } },
|
|
4426
|
+
{ value: "move", label: "Move / rename", path: { key: "blob", label: "Source blob", selectMode: "both", required: true }, dest: { key: "destBlob", label: "Destination blob", selectMode: "both", required: true } },
|
|
4427
|
+
{ value: "presign", label: "Get signed URL", path: { key: "blob", label: "File", selectMode: "both", required: true }, fields: [{ key: "expiresIn", label: "Expiry (seconds)", type: "number", defaultValue: 3600, advanced: true }] }
|
|
4428
|
+
]
|
|
4429
|
+
}
|
|
4430
|
+
};
|
|
4431
|
+
STORAGE_PROVIDERS.google_sheets = { ...STORAGE_PROVIDERS.google_drive, connectorId: "google_sheets" };
|
|
4432
|
+
var transferOp = {
|
|
4433
|
+
value: "transfer",
|
|
4434
|
+
label: "Transfer to another connector",
|
|
4435
|
+
path: { key: "path", label: "Source file or folder", selectMode: "both", required: true }
|
|
4436
|
+
};
|
|
4437
|
+
for (const id of ["aws_s3", "ftp_sftp", "dropbox", "onedrive", "google_drive", "gcs", "azure_blob"]) {
|
|
4438
|
+
STORAGE_PROVIDERS[id].operations.push(transferOp);
|
|
4439
|
+
}
|
|
4440
|
+
var syncOp = {
|
|
4441
|
+
value: "sync",
|
|
4442
|
+
label: "Sync folder to another connector",
|
|
4443
|
+
path: { key: "path", label: "Source folder", selectMode: "both", required: true },
|
|
4444
|
+
fields: [
|
|
4445
|
+
{
|
|
4446
|
+
key: "ifExists",
|
|
4447
|
+
label: "If a file already exists",
|
|
4448
|
+
type: "select",
|
|
4449
|
+
options: [
|
|
4450
|
+
{ label: "Overwrite", value: "overwrite" },
|
|
4451
|
+
{ label: "Skip it", value: "skip" },
|
|
4452
|
+
{ label: "Keep both (rename)", value: "rename" }
|
|
4453
|
+
],
|
|
4454
|
+
defaultValue: "overwrite",
|
|
4455
|
+
hint: "What to do when the destination already has the file."
|
|
4456
|
+
},
|
|
4457
|
+
{
|
|
4458
|
+
key: "onError",
|
|
4459
|
+
label: "On file error",
|
|
4460
|
+
type: "select",
|
|
4461
|
+
options: [
|
|
4462
|
+
{ label: "Stop the sync", value: "abort" },
|
|
4463
|
+
{ label: "Skip the file & continue", value: "skip" }
|
|
4464
|
+
],
|
|
4465
|
+
defaultValue: "abort",
|
|
4466
|
+
hint: "A bad file (timeout, lock, permission) stops the whole sync, or is skipped."
|
|
4467
|
+
}
|
|
4468
|
+
]
|
|
4714
4469
|
};
|
|
4470
|
+
for (const id of ["aws_s3", "ftp_sftp", "dropbox", "onedrive", "google_drive", "gcs", "azure_blob"]) {
|
|
4471
|
+
STORAGE_PROVIDERS[id].operations.push(syncOp);
|
|
4472
|
+
}
|
|
4473
|
+
var STORAGE_CONNECTOR_IDS = Object.keys(STORAGE_PROVIDERS).filter((id) => id !== "google_sheets");
|
|
4474
|
+
function getStorageProvider(connectorId) {
|
|
4475
|
+
if (!connectorId)
|
|
4476
|
+
return;
|
|
4477
|
+
return STORAGE_PROVIDERS[connectorId] ?? STORAGE_PROVIDERS[connectorId.replace(/-/g, "_")];
|
|
4478
|
+
}
|
|
4479
|
+
function storageConfigKeys() {
|
|
4480
|
+
const keys = new Set([
|
|
4481
|
+
"operation",
|
|
4482
|
+
"connector",
|
|
4483
|
+
"content",
|
|
4484
|
+
"outputAs",
|
|
4485
|
+
"path",
|
|
4486
|
+
"destPath",
|
|
4487
|
+
"destConnector",
|
|
4488
|
+
"sourceIsDirectory",
|
|
4489
|
+
"cursor",
|
|
4490
|
+
"fileBudget",
|
|
4491
|
+
"timeBudgetMs"
|
|
4492
|
+
]);
|
|
4493
|
+
for (const provider of Object.values(STORAGE_PROVIDERS)) {
|
|
4494
|
+
for (const op of provider.operations) {
|
|
4495
|
+
if (op.path)
|
|
4496
|
+
keys.add(op.path.key);
|
|
4497
|
+
if (op.dest)
|
|
4498
|
+
keys.add(op.dest.key);
|
|
4499
|
+
for (const f of op.fields ?? [])
|
|
4500
|
+
keys.add(f.key);
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
4503
|
+
return [...keys];
|
|
4504
|
+
}
|
|
4505
|
+
|
|
4715
4506
|
// src/catalog/storage/storage-action/config.ts
|
|
4716
4507
|
var FIXED_SCHEMA = {
|
|
4717
4508
|
connector: { type: "connection", required: true, description: "Storage connector instance; its provider decides the operations" },
|
|
@@ -4805,6 +4596,7 @@ var StorageActionConfig = {
|
|
|
4805
4596
|
merge: (userConfig) => ({ ...StorageActionConfig.defaults, ...userConfig }),
|
|
4806
4597
|
getLabel: (config) => `Storage: ${config.operation || "download"}`
|
|
4807
4598
|
};
|
|
4599
|
+
|
|
4808
4600
|
// src/catalog/storage/s3-action/config.ts
|
|
4809
4601
|
var S3_OPERATIONS = ["list", "download", "upload", "delete", "copy", "move", "presign"];
|
|
4810
4602
|
var S3ActionConfig = {
|
|
@@ -4842,6 +4634,7 @@ var S3ActionConfig = {
|
|
|
4842
4634
|
merge: (userConfig) => ({ ...S3ActionConfig.defaults, ...userConfig }),
|
|
4843
4635
|
getLabel: (config) => `S3: ${config.operation || "download"}`
|
|
4844
4636
|
};
|
|
4637
|
+
|
|
4845
4638
|
// src/catalog/storage/ftp-action/config.ts
|
|
4846
4639
|
var FTP_OPERATIONS = ["download", "upload", "list", "delete", "move"];
|
|
4847
4640
|
var FtpActionConfig = {
|
|
@@ -4872,6 +4665,7 @@ var FtpActionConfig = {
|
|
|
4872
4665
|
merge: (userConfig) => ({ ...FtpActionConfig.defaults, ...userConfig }),
|
|
4873
4666
|
getLabel: (config) => `FTP: ${config.operation || "download"}`
|
|
4874
4667
|
};
|
|
4668
|
+
|
|
4875
4669
|
// src/catalog/storage/google-drive-action/config.ts
|
|
4876
4670
|
var GOOGLE_DRIVE_OPERATIONS = ["upload", "download", "list", "delete", "move", "copy", "share", "create_folder"];
|
|
4877
4671
|
var GoogleDriveActionConfig = {
|
|
@@ -4913,6 +4707,7 @@ var GoogleDriveActionConfig = {
|
|
|
4913
4707
|
merge: (userConfig) => ({ ...GoogleDriveActionConfig.defaults, ...userConfig }),
|
|
4914
4708
|
getLabel: (config) => `Drive: ${config.operation || "upload"}`
|
|
4915
4709
|
};
|
|
4710
|
+
|
|
4916
4711
|
// src/catalog/storage/dropbox-action/config.ts
|
|
4917
4712
|
var DROPBOX_OPERATIONS = ["list", "download", "upload", "delete", "move", "copy"];
|
|
4918
4713
|
var DropboxActionConfig = {
|
|
@@ -4943,6 +4738,7 @@ var DropboxActionConfig = {
|
|
|
4943
4738
|
merge: (userConfig) => ({ ...DropboxActionConfig.defaults, ...userConfig }),
|
|
4944
4739
|
getLabel: (config) => `Dropbox: ${config.operation || "download"}`
|
|
4945
4740
|
};
|
|
4741
|
+
|
|
4946
4742
|
// src/catalog/storage/onedrive-action/config.ts
|
|
4947
4743
|
var ONEDRIVE_OPERATIONS = ["download", "upload", "list", "delete", "move", "copy"];
|
|
4948
4744
|
var OneDriveActionConfig = {
|
|
@@ -4974,881 +4770,1141 @@ var OneDriveActionConfig = {
|
|
|
4974
4770
|
merge: (userConfig) => ({ ...OneDriveActionConfig.defaults, ...userConfig }),
|
|
4975
4771
|
getLabel: (config) => `OneDrive: ${config.operation || "download"}`
|
|
4976
4772
|
};
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
((
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
(
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
4773
|
+
|
|
4774
|
+
// src/rules/configContracts.ts
|
|
4775
|
+
var asValidator = (fn) => (config) => fn(config);
|
|
4776
|
+
var CONFIG_CONTRACTS = {
|
|
4777
|
+
console_log: { schema: ConsoleLogConfig.schema, validate: asValidator(ConsoleLogConfig.validate) },
|
|
4778
|
+
delay: { schema: DelayConfig.schema, validate: asValidator(DelayConfig.validate) },
|
|
4779
|
+
http_request: { schema: HTTPRequestConfig.schema, validate: asValidator(HTTPRequestConfig.validate) },
|
|
4780
|
+
email: { schema: EmailConfig.schema, validate: asValidator(EmailConfig.validate) },
|
|
4781
|
+
google_chat: { schema: GoogleChatConfig.schema, validate: asValidator(GoogleChatConfig.validate) },
|
|
4782
|
+
slack_message: { schema: SlackMessageConfig.schema, validate: asValidator(SlackMessageConfig.validate) },
|
|
4783
|
+
teams_message: { schema: TeamsMessageConfig.schema, validate: asValidator(TeamsMessageConfig.validate) },
|
|
4784
|
+
webhook_call: { schema: WebhookCallConfig.schema, validate: asValidator(WebhookCallConfig.validate) },
|
|
4785
|
+
whatsapp_send: { schema: WhatsappSendConfig.schema, validate: asValidator(WhatsappSendConfig.validate) },
|
|
4786
|
+
query: {
|
|
4787
|
+
schema: BuildQueryContractSchema,
|
|
4788
|
+
validate: (config) => {
|
|
4789
|
+
const r = validateBuildQueryConfig(config);
|
|
4790
|
+
const errors = (r.errors ?? []).filter((e) => e.severity !== "warning").map((e) => e.message);
|
|
4791
|
+
return { valid: errors.length === 0, errors };
|
|
4792
|
+
}
|
|
4793
|
+
},
|
|
4794
|
+
transform: { schema: TransformConfig.schema, validate: asValidator(TransformConfig.validate) },
|
|
4795
|
+
aggregate: { schema: AggregateConfig.schema, validate: asValidator(AggregateConfig.validate) },
|
|
4796
|
+
condition: { schema: ConditionConfig.schema, validate: asValidator(ConditionConfig.validate) },
|
|
4797
|
+
for_each: { schema: ForEachConfig.schema, validate: asValidator(ForEachConfig.validate) },
|
|
4798
|
+
manual_trigger: { schema: ManualTriggerConfig.schema, validate: asValidator(ManualTriggerConfig.validate) },
|
|
4799
|
+
schedule_trigger: { schema: ScheduleTriggerConfig.schema, validate: asValidator(ScheduleTriggerConfig.validate) },
|
|
4800
|
+
webhook_trigger: { schema: WebhookTriggerConfig.schema, validate: asValidator(WebhookTriggerConfig.validate) },
|
|
4801
|
+
debug: { schema: DebugConfig.schema, validate: asValidator(DebugConfig.validate) },
|
|
4802
|
+
variables: { schema: VariablesConfig.schema, validate: asValidator(VariablesConfig.validate) },
|
|
4803
|
+
ai_agent: { schema: AiAgentConfig.schema, validate: asValidator(AiAgentConfig.validate) },
|
|
4804
|
+
idp_extract: { schema: IdpExtractConfig.schema, validate: asValidator(IdpExtractConfig.validate) },
|
|
4805
|
+
group: { schema: GroupConfig.schema, validate: asValidator(GroupConfig.validate) },
|
|
4806
|
+
connector_trigger: { schema: ConnectorTriggerConfig.schema, validate: asValidator(ConnectorTriggerConfig.validate) },
|
|
4807
|
+
csv_action: { schema: CsvActionConfig.schema, validate: asValidator(CsvActionConfig.validate) },
|
|
4808
|
+
json_action: { schema: JsonActionConfig.schema, validate: asValidator(JsonActionConfig.validate) },
|
|
4809
|
+
xml_action: { schema: XmlActionConfig.schema, validate: asValidator(XmlActionConfig.validate) },
|
|
4810
|
+
excel_action: { schema: ExcelActionConfig.schema, validate: asValidator(ExcelActionConfig.validate) },
|
|
4811
|
+
storage_action: { schema: StorageActionConfig.schema, validate: asValidator(StorageActionConfig.validate) },
|
|
4812
|
+
s3_action: { schema: S3ActionConfig.schema, validate: asValidator(S3ActionConfig.validate) },
|
|
4813
|
+
ftp_action: { schema: FtpActionConfig.schema, validate: asValidator(FtpActionConfig.validate) },
|
|
4814
|
+
google_drive_action: { schema: GoogleDriveActionConfig.schema, validate: asValidator(GoogleDriveActionConfig.validate) },
|
|
4815
|
+
dropbox_action: { schema: DropboxActionConfig.schema, validate: asValidator(DropboxActionConfig.validate) },
|
|
4816
|
+
onedrive_action: { schema: OneDriveActionConfig.schema, validate: asValidator(OneDriveActionConfig.validate) }
|
|
4817
|
+
};
|
|
4818
|
+
function contractFor(actionId) {
|
|
4819
|
+
return actionId ? CONFIG_CONTRACTS[actionId] : undefined;
|
|
4820
|
+
}
|
|
4821
|
+
function declaredKeys(actionId) {
|
|
4822
|
+
const c = contractFor(actionId);
|
|
4823
|
+
if (!c)
|
|
4824
|
+
return [];
|
|
4825
|
+
return Object.entries(c.schema).filter(([, f]) => f.type !== "none").map(([k]) => k);
|
|
4826
|
+
}
|
|
4827
|
+
function isEditorPrivateKey(key) {
|
|
4828
|
+
return key.startsWith("_");
|
|
4829
|
+
}
|
|
4830
|
+
var CONTRACT_TYPE_TO_FIELD_TYPE = {
|
|
4831
|
+
string: "string" /* String */,
|
|
4832
|
+
text: "string" /* String */,
|
|
4833
|
+
select: "select" /* Select */,
|
|
4834
|
+
json: "json" /* JSON */,
|
|
4835
|
+
connection: "connection" /* Connection */,
|
|
4836
|
+
number: "number" /* Number */,
|
|
4837
|
+
boolean: "boolean" /* Boolean */,
|
|
4838
|
+
array: "array" /* Array */,
|
|
4839
|
+
object: "object" /* Object */,
|
|
4840
|
+
url: "url" /* URL */,
|
|
4841
|
+
expression: "expression" /* Expression */,
|
|
4842
|
+
code: "code" /* Code */
|
|
4843
|
+
};
|
|
4844
|
+
function humanise(key) {
|
|
4845
|
+
return key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").replace(/^\w/, (c) => c.toUpperCase());
|
|
4846
|
+
}
|
|
4847
|
+
function contractFieldsFor(definition) {
|
|
4848
|
+
const declared = definition.inputSchema?.fields ?? [];
|
|
4849
|
+
const contract = contractFor(definition.id);
|
|
4850
|
+
if (!contract)
|
|
4851
|
+
return declared;
|
|
4852
|
+
const seen = new Set(declared.map((f) => f.key));
|
|
4853
|
+
const derived = [];
|
|
4854
|
+
for (const [key, field] of Object.entries(contract.schema)) {
|
|
4855
|
+
if (seen.has(key) || field.type === "none")
|
|
4856
|
+
continue;
|
|
4857
|
+
derived.push({
|
|
4858
|
+
key,
|
|
4859
|
+
label: humanise(key),
|
|
4860
|
+
type: CONTRACT_TYPE_TO_FIELD_TYPE[field.type] ?? "string" /* String */,
|
|
4861
|
+
validation: { required: field.required === true },
|
|
4862
|
+
...typeof field.description === "string" && field.description ? { description: field.description } : {},
|
|
4863
|
+
...field.options ? { options: field.options.map((v) => ({ label: String(v), value: v })) } : {}
|
|
4864
|
+
});
|
|
4865
|
+
}
|
|
4866
|
+
return [...declared, ...derived];
|
|
4867
|
+
}
|
|
4868
|
+
function selectsWithoutValues(actionId) {
|
|
4869
|
+
const contract = contractFor(actionId);
|
|
4870
|
+
if (!contract)
|
|
4871
|
+
return [];
|
|
4872
|
+
return Object.entries(contract.schema).filter(([, f]) => f.type === "select" && !(f.options && f.options.length > 0) && f.dynamic !== true).map(([key]) => key);
|
|
4873
|
+
}
|
|
4874
|
+
function contractDrift(definition) {
|
|
4875
|
+
const contract = contractFor(definition.id);
|
|
4876
|
+
if (!contract)
|
|
4877
|
+
return [];
|
|
4878
|
+
const covers = (key) => {
|
|
4879
|
+
if (key in contract.schema)
|
|
4880
|
+
return true;
|
|
4881
|
+
const root = key.split(".")[0];
|
|
4882
|
+
const rootField = contract.schema[root];
|
|
4883
|
+
return !!rootField && (rootField.type === "object" || rootField.type === "json");
|
|
4884
|
+
};
|
|
4885
|
+
return (definition.inputSchema?.fields ?? []).map((f) => f.key).filter((k) => !covers(k));
|
|
4886
|
+
}
|
|
4887
|
+
function contractVocabularyDrift(actionId) {
|
|
4888
|
+
const c = contractFor(actionId);
|
|
4889
|
+
if (!c)
|
|
4890
|
+
return [];
|
|
4891
|
+
const known = new Set([...Object.keys(CONTRACT_TYPE_TO_FIELD_TYPE), "none"]);
|
|
4892
|
+
return Object.entries(c.schema).filter(([, f]) => !known.has(f.type)).map(([k, f]) => `${k}:${f.type}`);
|
|
4893
|
+
}
|
|
4894
|
+
function contractDerivedKeys(definition) {
|
|
4895
|
+
const own = new Set((definition.inputSchema?.fields ?? []).map((f) => f.key));
|
|
4896
|
+
return new Set(contractFieldsFor(definition).map((f) => f.key).filter((k) => !own.has(k)));
|
|
4897
|
+
}
|
|
4898
|
+
|
|
4899
|
+
// src/rules/nodeConfigStatus.ts
|
|
4900
|
+
var HIDDEN_STATUS_ACTIONS = new Set(["console_log"]);
|
|
4901
|
+
function isFieldFilled(value) {
|
|
4902
|
+
if (value === undefined || value === null)
|
|
4903
|
+
return false;
|
|
4904
|
+
if (typeof value === "string")
|
|
4905
|
+
return value.trim().length > 0;
|
|
4906
|
+
if (typeof value === "number")
|
|
4907
|
+
return true;
|
|
4908
|
+
if (typeof value === "boolean")
|
|
4909
|
+
return true;
|
|
4910
|
+
if (Array.isArray(value))
|
|
4911
|
+
return value.length > 0;
|
|
4912
|
+
if (typeof value === "object")
|
|
4913
|
+
return Object.keys(value).length > 0;
|
|
4914
|
+
return false;
|
|
4915
|
+
}
|
|
4916
|
+
function isSatisfiedOutOfBand(actionId, fieldKey, config) {
|
|
4917
|
+
if (actionId === "connector_trigger" && fieldKey === "events") {
|
|
4918
|
+
return Number(config.eventCount ?? 0) > 0;
|
|
4919
|
+
}
|
|
4920
|
+
if (MESSAGE_BODY_ACTIONS.has(actionId) && (fieldKey === "message" || fieldKey === "text")) {
|
|
4921
|
+
const text = config.text ?? config.message;
|
|
4922
|
+
if (typeof text === "string" && text.trim().length > 0)
|
|
5000
4923
|
return true;
|
|
4924
|
+
return Array.isArray(config.blocks) && config.blocks.length > 0;
|
|
5001
4925
|
}
|
|
4926
|
+
if (actionId === "query" && fieldKey === "visualConfig.fields")
|
|
4927
|
+
return queryFieldsSatisfied(config);
|
|
4928
|
+
if (actionId === "query" && fieldKey === "visualConfig.tables")
|
|
4929
|
+
return queryTablesSatisfied(config);
|
|
5002
4930
|
return false;
|
|
5003
4931
|
}
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
const
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
...parentIdForSubflow ? { parentId: parentIdForSubflow } : {}
|
|
5016
|
-
};
|
|
5017
|
-
flatSteps.push(flatStep);
|
|
5018
|
-
if (!branches)
|
|
5019
|
-
continue;
|
|
5020
|
-
if (branches.body && step.type === "group") {
|
|
5021
|
-
const body = branches.body;
|
|
5022
|
-
groups.push({
|
|
5023
|
-
group: flatStep,
|
|
5024
|
-
bodyStepIds: body.steps.map((s) => s.id),
|
|
5025
|
-
bodyConnections: body.connections
|
|
5026
|
-
});
|
|
5027
|
-
for (const c of body.connections)
|
|
5028
|
-
flatConns.push(c);
|
|
5029
|
-
walk(body.steps, step.id);
|
|
5030
|
-
}
|
|
5031
|
-
for (const branchKey of Object.keys(branches)) {
|
|
5032
|
-
if (branchKey === "body" && step.type === "group")
|
|
5033
|
-
continue;
|
|
5034
|
-
const sub = branches[branchKey];
|
|
5035
|
-
if (!sub)
|
|
5036
|
-
continue;
|
|
5037
|
-
for (const c of sub.connections)
|
|
5038
|
-
flatConns.push(c);
|
|
5039
|
-
walk(sub.steps, parentIdForSubflow);
|
|
5040
|
-
}
|
|
4932
|
+
var MESSAGE_BODY_ACTIONS = new Set([
|
|
4933
|
+
"slack_message",
|
|
4934
|
+
"teams_message",
|
|
4935
|
+
"google_chat"
|
|
4936
|
+
]);
|
|
4937
|
+
function getNestedValue(config, keyPath) {
|
|
4938
|
+
const parts = keyPath.split(".");
|
|
4939
|
+
let current = config;
|
|
4940
|
+
for (const part of parts) {
|
|
4941
|
+
if (current === null || current === undefined || typeof current !== "object") {
|
|
4942
|
+
return;
|
|
5041
4943
|
}
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
4944
|
+
current = current[part];
|
|
4945
|
+
}
|
|
4946
|
+
return current;
|
|
4947
|
+
}
|
|
4948
|
+
function isFieldActive(field, config) {
|
|
4949
|
+
if (!field.conditional || field.conditional.length === 0) {
|
|
4950
|
+
return true;
|
|
4951
|
+
}
|
|
4952
|
+
return field.conditional.some((cond) => {
|
|
4953
|
+
if (cond.action !== "show" && cond.action !== "require") {
|
|
4954
|
+
return false;
|
|
5053
4955
|
}
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
4956
|
+
const fieldValue = getNestedValue(config, cond.field);
|
|
4957
|
+
switch (cond.operator) {
|
|
4958
|
+
case "==":
|
|
4959
|
+
return fieldValue === cond.value;
|
|
4960
|
+
case "!=":
|
|
4961
|
+
return fieldValue !== cond.value;
|
|
4962
|
+
case ">":
|
|
4963
|
+
return fieldValue > cond.value;
|
|
4964
|
+
case "<":
|
|
4965
|
+
return fieldValue < cond.value;
|
|
4966
|
+
case ">=":
|
|
4967
|
+
return fieldValue >= cond.value;
|
|
4968
|
+
case "<=":
|
|
4969
|
+
return fieldValue <= cond.value;
|
|
4970
|
+
case "in":
|
|
4971
|
+
return Array.isArray(cond.value) && cond.value.includes(fieldValue);
|
|
4972
|
+
case "not_in":
|
|
4973
|
+
return Array.isArray(cond.value) && !cond.value.includes(fieldValue);
|
|
4974
|
+
case "contains":
|
|
4975
|
+
return typeof fieldValue === "string" && fieldValue.includes(cond.value);
|
|
4976
|
+
default:
|
|
4977
|
+
return false;
|
|
5061
4978
|
}
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
4979
|
+
});
|
|
4980
|
+
}
|
|
4981
|
+
function getNodeConfigStatus(node) {
|
|
4982
|
+
if (HIDDEN_STATUS_ACTIONS.has(node.actionDefinitionId)) {
|
|
4983
|
+
return {
|
|
4984
|
+
status: "hidden",
|
|
4985
|
+
requiredTotal: 0,
|
|
4986
|
+
requiredFilled: 0,
|
|
4987
|
+
optionalTotal: 0,
|
|
4988
|
+
optionalFilled: 0,
|
|
4989
|
+
missingRequired: [],
|
|
4990
|
+
filledOptional: []
|
|
4991
|
+
};
|
|
4992
|
+
}
|
|
4993
|
+
const definition = getActionById(node.actionDefinitionId);
|
|
4994
|
+
if (!definition) {
|
|
4995
|
+
return {
|
|
4996
|
+
status: "unconfigured",
|
|
4997
|
+
requiredTotal: 0,
|
|
4998
|
+
requiredFilled: 0,
|
|
4999
|
+
optionalTotal: 0,
|
|
5000
|
+
optionalFilled: 0,
|
|
5001
|
+
missingRequired: [],
|
|
5002
|
+
filledOptional: []
|
|
5003
|
+
};
|
|
5004
|
+
}
|
|
5005
|
+
const fields = definition.inputSchema?.fields ?? [];
|
|
5006
|
+
const config = node.config ?? {};
|
|
5007
|
+
const activeFields = fields.filter((f) => !f.hidden && isFieldActive(f, config));
|
|
5008
|
+
const requiredFields = activeFields.filter((f) => f.validation?.required);
|
|
5009
|
+
const optionalFields = activeFields.filter((f) => !f.validation?.required);
|
|
5010
|
+
const missingRequired = [];
|
|
5011
|
+
let requiredFilled = 0;
|
|
5012
|
+
for (const field of requiredFields) {
|
|
5013
|
+
const value = getNestedValue(config, field.key);
|
|
5014
|
+
if (isFieldFilled(value)) {
|
|
5015
|
+
requiredFilled++;
|
|
5016
|
+
} else if (isFieldFilled(field.defaultValue)) {
|
|
5017
|
+
requiredFilled++;
|
|
5018
|
+
} else if (isSatisfiedOutOfBand(node.actionDefinitionId, field.key, config)) {
|
|
5019
|
+
requiredFilled++;
|
|
5020
|
+
} else {
|
|
5021
|
+
missingRequired.push(field.key);
|
|
5071
5022
|
}
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5023
|
+
}
|
|
5024
|
+
const filledOptional = [];
|
|
5025
|
+
let optionalFilled = 0;
|
|
5026
|
+
for (const field of optionalFields) {
|
|
5027
|
+
const value = getNestedValue(config, field.key);
|
|
5028
|
+
if (isFieldFilled(value)) {
|
|
5029
|
+
optionalFilled++;
|
|
5030
|
+
filledOptional.push(field.key);
|
|
5079
5031
|
}
|
|
5080
5032
|
}
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
case "executing":
|
|
5093
|
-
nodeStatus = "running" /* Running */;
|
|
5094
|
-
break;
|
|
5095
|
-
case "completed":
|
|
5096
|
-
case "success":
|
|
5097
|
-
nodeStatus = "success" /* Success */;
|
|
5098
|
-
break;
|
|
5099
|
-
case "failed":
|
|
5100
|
-
case "error":
|
|
5101
|
-
nodeStatus = "error" /* Error */;
|
|
5102
|
-
break;
|
|
5103
|
-
case "pending":
|
|
5104
|
-
case "waiting":
|
|
5105
|
-
nodeStatus = "idle" /* Idle */;
|
|
5106
|
-
break;
|
|
5107
|
-
default:
|
|
5108
|
-
nodeStatus = "idle" /* Idle */;
|
|
5033
|
+
const requiredTotal = requiredFields.length;
|
|
5034
|
+
const optionalTotal = optionalFields.length;
|
|
5035
|
+
let status;
|
|
5036
|
+
if (requiredTotal === 0) {
|
|
5037
|
+
status = "valid";
|
|
5038
|
+
} else if (missingRequired.length === 0) {
|
|
5039
|
+
status = "valid";
|
|
5040
|
+
} else if (requiredFilled > 0 || optionalFilled > 0) {
|
|
5041
|
+
status = "warning";
|
|
5042
|
+
} else {
|
|
5043
|
+
status = "error";
|
|
5109
5044
|
}
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
const node = {
|
|
5119
|
-
id: step.id,
|
|
5120
|
-
type: nodeType,
|
|
5121
|
-
actionDefinitionId: resolvedActionDefinitionId,
|
|
5122
|
-
actionDefinition: actionDef,
|
|
5123
|
-
label: step.label || step.name,
|
|
5124
|
-
description: step.description || config.description || actionDef?.description || "",
|
|
5125
|
-
icon: actionDef?.icon ?? getIconForStepType(step.type),
|
|
5126
|
-
position: {
|
|
5127
|
-
x: posX,
|
|
5128
|
-
y: posY
|
|
5129
|
-
},
|
|
5130
|
-
...step.parentId ? { parentId: step.parentId } : {},
|
|
5131
|
-
...step.extent ? { extent: step.extent } : {},
|
|
5132
|
-
...step.disabled ? { disabled: true } : {},
|
|
5133
|
-
...step.dimensions && nodeType === "group" ? { dimensions: step.dimensions } : nodeType === "group" ? { dimensions: { width: 480, height: 240 } } : {},
|
|
5134
|
-
config: {
|
|
5135
|
-
...configData,
|
|
5136
|
-
timeout: step.timeout_seconds,
|
|
5137
|
-
retryPolicy: step.retry_policy,
|
|
5138
|
-
notes: step.notes,
|
|
5139
|
-
tags: step.tags,
|
|
5140
|
-
stepCount: step.step_count
|
|
5141
|
-
},
|
|
5142
|
-
ports: {
|
|
5143
|
-
input: nodeType === "action" || nodeType === "group" ? [
|
|
5144
|
-
{
|
|
5145
|
-
id: `${step.id}-input`,
|
|
5146
|
-
type: "input",
|
|
5147
|
-
position: "top",
|
|
5148
|
-
connected: false
|
|
5149
|
-
}
|
|
5150
|
-
] : [],
|
|
5151
|
-
output: (() => {
|
|
5152
|
-
if (resolvedActionDefinitionId === "condition") {
|
|
5153
|
-
return [
|
|
5154
|
-
{ id: `${step.id}_output_true`, type: "output", position: "bottom", label: "True", connected: false },
|
|
5155
|
-
{ id: `${step.id}_output_false`, type: "output", position: "bottom", label: "False", connected: false }
|
|
5156
|
-
];
|
|
5157
|
-
}
|
|
5158
|
-
if (resolvedActionDefinitionId === "for_each" || resolvedActionDefinitionId === "for-each") {
|
|
5159
|
-
return [
|
|
5160
|
-
{ id: `${step.id}_output_each`, type: "output", position: "bottom", label: "Each", connected: false },
|
|
5161
|
-
{ id: `${step.id}_output_done`, type: "output", position: "bottom", label: "Done", connected: false }
|
|
5162
|
-
];
|
|
5163
|
-
}
|
|
5164
|
-
if (resolvedActionDefinitionId === "group") {
|
|
5165
|
-
return [
|
|
5166
|
-
{ id: `${step.id}_output_0`, type: "output", position: "bottom", connected: false }
|
|
5167
|
-
];
|
|
5168
|
-
}
|
|
5169
|
-
return [
|
|
5170
|
-
{ id: `${step.id}-output`, type: "output", position: "bottom", connected: false }
|
|
5171
|
-
];
|
|
5172
|
-
})()
|
|
5173
|
-
},
|
|
5174
|
-
configStatus: step.configStatus || undefined,
|
|
5175
|
-
...step.outputSchema ? { outputSchema: step.outputSchema } : {},
|
|
5176
|
-
status: nodeStatus,
|
|
5177
|
-
selected: false,
|
|
5178
|
-
dragging: false,
|
|
5179
|
-
isValid: step.configStatus ? step.configStatus === "valid" : true,
|
|
5180
|
-
expanded: step.expanded,
|
|
5181
|
-
minimized: false,
|
|
5182
|
-
locked: false,
|
|
5183
|
-
createdAt: step.created_at || new Date().toISOString(),
|
|
5184
|
-
updatedAt: step.updated_at || new Date().toISOString()
|
|
5045
|
+
return {
|
|
5046
|
+
status,
|
|
5047
|
+
requiredTotal,
|
|
5048
|
+
requiredFilled,
|
|
5049
|
+
optionalTotal,
|
|
5050
|
+
optionalFilled,
|
|
5051
|
+
missingRequired,
|
|
5052
|
+
filledOptional
|
|
5185
5053
|
};
|
|
5186
|
-
return node;
|
|
5187
5054
|
}
|
|
5188
|
-
function
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
const
|
|
5192
|
-
const
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5055
|
+
function missingContractRequired(node) {
|
|
5056
|
+
if (HIDDEN_STATUS_ACTIONS.has(node.actionDefinitionId))
|
|
5057
|
+
return [];
|
|
5058
|
+
const definition = getActionById(node.actionDefinitionId);
|
|
5059
|
+
const contract = contractFor(node.actionDefinitionId);
|
|
5060
|
+
if (!definition || !contract)
|
|
5061
|
+
return [];
|
|
5062
|
+
const requiredInSchema = new Set((definition.inputSchema?.fields ?? []).filter((f) => f.validation?.required).map((f) => f.key));
|
|
5063
|
+
const config = node.config ?? {};
|
|
5064
|
+
const missing = [];
|
|
5065
|
+
for (const [key, field] of Object.entries(contract.schema)) {
|
|
5066
|
+
if (field.type === "none" || field.required !== true || requiredInSchema.has(key))
|
|
5067
|
+
continue;
|
|
5068
|
+
if (isFieldFilled(getNestedValue(config, key)))
|
|
5069
|
+
continue;
|
|
5070
|
+
if (isSatisfiedOutOfBand(node.actionDefinitionId, key, config))
|
|
5071
|
+
continue;
|
|
5072
|
+
missing.push(key);
|
|
5196
5073
|
}
|
|
5197
|
-
return
|
|
5198
|
-
id: connection.id,
|
|
5199
|
-
source: sourceStepId,
|
|
5200
|
-
target: targetStepId,
|
|
5201
|
-
sourcePort,
|
|
5202
|
-
targetPort,
|
|
5203
|
-
type: edgeType,
|
|
5204
|
-
label: connection.label,
|
|
5205
|
-
selected: false,
|
|
5206
|
-
animated: false,
|
|
5207
|
-
createdAt: new Date().toISOString()
|
|
5208
|
-
};
|
|
5074
|
+
return missing;
|
|
5209
5075
|
}
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
id:
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5076
|
+
|
|
5077
|
+
// src/rules/workflowValidator.ts
|
|
5078
|
+
var nodeName = (n) => n.label || n.id;
|
|
5079
|
+
function validateNode(node, allNodes, allEdges) {
|
|
5080
|
+
const errors = [];
|
|
5081
|
+
if (!node.id || node.id.trim().length === 0) {
|
|
5082
|
+
errors.push({
|
|
5083
|
+
id: `node-${node.id}-id`,
|
|
5084
|
+
type: "node",
|
|
5085
|
+
severity: "error",
|
|
5086
|
+
message: "Node ID is required",
|
|
5087
|
+
nodeId: node.id
|
|
5088
|
+
});
|
|
5089
|
+
}
|
|
5090
|
+
if (!node.label || node.label.trim().length === 0) {
|
|
5091
|
+
errors.push({
|
|
5092
|
+
id: `node-${node.id}-label`,
|
|
5093
|
+
type: "node",
|
|
5094
|
+
severity: "error",
|
|
5095
|
+
message: "Node label is required",
|
|
5096
|
+
nodeId: node.id
|
|
5097
|
+
});
|
|
5098
|
+
}
|
|
5099
|
+
const cfg = getNodeConfigStatus(node);
|
|
5100
|
+
if (cfg.missingRequired.length > 0) {
|
|
5101
|
+
const fields = cfg.missingRequired.slice(0, 4).join(", ");
|
|
5102
|
+
const more = cfg.missingRequired.length > 4 ? ` +${cfg.missingRequired.length - 4} more` : "";
|
|
5103
|
+
errors.push({
|
|
5104
|
+
id: `node-${node.id}-config`,
|
|
5105
|
+
type: "node",
|
|
5106
|
+
severity: "error",
|
|
5107
|
+
message: `Missing required settings: ${fields}${more}. Open the step and fill them in.`,
|
|
5108
|
+
nodeId: node.id
|
|
5109
|
+
});
|
|
5110
|
+
}
|
|
5111
|
+
const contractGaps = missingContractRequired(node);
|
|
5112
|
+
if (contractGaps.length > 0) {
|
|
5113
|
+
const fields = contractGaps.slice(0, 4).join(", ");
|
|
5114
|
+
const more = contractGaps.length > 4 ? ` +${contractGaps.length - 4} more` : "";
|
|
5115
|
+
errors.push({
|
|
5116
|
+
id: `node-${node.id}-contract`,
|
|
5117
|
+
type: "node",
|
|
5118
|
+
severity: "warning",
|
|
5119
|
+
message: `Not set: ${fields}${more}. This step needs ${contractGaps.length === 1 ? "it" : "them"} to run. Open the step and fill ${contractGaps.length === 1 ? "it" : "them"} in.`,
|
|
5120
|
+
nodeId: node.id
|
|
5121
|
+
});
|
|
5122
|
+
}
|
|
5123
|
+
const hasOutput = allEdges.some((edge) => edge.source === node.id);
|
|
5124
|
+
if (!hasOutput && node.type === "trigger") {
|
|
5125
|
+
errors.push({
|
|
5126
|
+
id: `node-${node.id}-output`,
|
|
5127
|
+
type: "node",
|
|
5128
|
+
severity: "warning",
|
|
5129
|
+
message: `Trigger "${nodeName(node)}" has no outgoing connections`,
|
|
5130
|
+
nodeId: node.id
|
|
5131
|
+
});
|
|
5132
|
+
}
|
|
5133
|
+
return errors;
|
|
5134
|
+
}
|
|
5135
|
+
function validateEdge(edge, allNodes, allEdges = []) {
|
|
5136
|
+
const errors = [];
|
|
5137
|
+
const sourceNode = allNodes.find((n) => n.id === edge.source);
|
|
5138
|
+
if (!sourceNode) {
|
|
5139
|
+
errors.push({
|
|
5140
|
+
id: `edge-${edge.id}-source`,
|
|
5141
|
+
type: "edge",
|
|
5142
|
+
severity: "error",
|
|
5143
|
+
message: `Source node "${edge.source}" does not exist`,
|
|
5144
|
+
edgeId: edge.id
|
|
5145
|
+
});
|
|
5146
|
+
}
|
|
5147
|
+
const targetNode = allNodes.find((n) => n.id === edge.target);
|
|
5148
|
+
if (!targetNode) {
|
|
5149
|
+
errors.push({
|
|
5150
|
+
id: `edge-${edge.id}-target`,
|
|
5151
|
+
type: "edge",
|
|
5152
|
+
severity: "error",
|
|
5153
|
+
message: `Target node "${edge.target}" does not exist`,
|
|
5154
|
+
edgeId: edge.id
|
|
5155
|
+
});
|
|
5156
|
+
}
|
|
5157
|
+
if (edge.source === edge.target) {
|
|
5158
|
+
errors.push({
|
|
5159
|
+
id: `edge-${edge.id}-loop`,
|
|
5160
|
+
type: "edge",
|
|
5161
|
+
severity: "error",
|
|
5162
|
+
message: "Nodes cannot connect to themselves",
|
|
5163
|
+
edgeId: edge.id
|
|
5164
|
+
});
|
|
5165
|
+
}
|
|
5166
|
+
const key = (e) => `${e.source}|${e.sourceHandle ?? ""}→${e.target}|${e.targetHandle ?? ""}`;
|
|
5167
|
+
const myKey = key(edge);
|
|
5168
|
+
const firstIndex = allEdges.findIndex((e) => key(e) === myKey);
|
|
5169
|
+
const myIndex = allEdges.findIndex((e) => e.id === edge.id);
|
|
5170
|
+
if (firstIndex !== -1 && myIndex > firstIndex) {
|
|
5171
|
+
errors.push({
|
|
5172
|
+
id: `edge-${edge.id}-duplicate`,
|
|
5173
|
+
type: "edge",
|
|
5174
|
+
severity: "warning",
|
|
5175
|
+
message: `Duplicate connection between "${sourceNode ? nodeName(sourceNode) : edge.source}" and "${targetNode ? nodeName(targetNode) : edge.target}"`,
|
|
5176
|
+
edgeId: edge.id
|
|
5223
5177
|
});
|
|
5224
5178
|
}
|
|
5225
|
-
return
|
|
5226
|
-
}
|
|
5227
|
-
function getIconForStepType(type) {
|
|
5228
|
-
const iconMap = {
|
|
5229
|
-
trigger: "PlayOutline",
|
|
5230
|
-
transform: "Transform",
|
|
5231
|
-
action: "Application",
|
|
5232
|
-
condition: "DecisionTree",
|
|
5233
|
-
loop: "Repeat"
|
|
5234
|
-
};
|
|
5235
|
-
return iconMap[type] || "Application";
|
|
5179
|
+
return errors;
|
|
5236
5180
|
}
|
|
5237
|
-
function
|
|
5238
|
-
const
|
|
5239
|
-
const
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5181
|
+
function validateWorkflow(nodes, edges) {
|
|
5182
|
+
const allErrors = [];
|
|
5183
|
+
const hasTrigger = nodes.some((node) => node.type === "trigger");
|
|
5184
|
+
if (!hasTrigger) {
|
|
5185
|
+
allErrors.push({
|
|
5186
|
+
id: "workflow-trigger",
|
|
5187
|
+
type: "workflow",
|
|
5188
|
+
severity: "error",
|
|
5189
|
+
message: "Workflow must have at least one trigger node"
|
|
5190
|
+
});
|
|
5245
5191
|
}
|
|
5246
|
-
const
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5192
|
+
const hasAction = nodes.some((node) => node.type === "action");
|
|
5193
|
+
if (!hasAction && nodes.length > 0) {
|
|
5194
|
+
allErrors.push({
|
|
5195
|
+
id: "workflow-action",
|
|
5196
|
+
type: "workflow",
|
|
5197
|
+
severity: "warning",
|
|
5198
|
+
message: "Workflow should have at least one action node"
|
|
5199
|
+
});
|
|
5200
|
+
}
|
|
5201
|
+
nodes.forEach((node) => {
|
|
5202
|
+
const nodeErrors = validateNode(node, nodes, edges);
|
|
5203
|
+
allErrors.push(...nodeErrors);
|
|
5251
5204
|
});
|
|
5252
5205
|
edges.forEach((edge) => {
|
|
5253
|
-
const
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5206
|
+
const edgeErrors = validateEdge(edge, nodes, edges);
|
|
5207
|
+
allErrors.push(...edgeErrors);
|
|
5208
|
+
});
|
|
5209
|
+
if (nodes.length > 0 && edges.length > 0) {
|
|
5210
|
+
const inDegree = new Map(nodes.map((n) => [n.id, 0]));
|
|
5211
|
+
const out = new Map;
|
|
5212
|
+
for (const e of edges) {
|
|
5213
|
+
if (e.source === e.target)
|
|
5214
|
+
continue;
|
|
5215
|
+
if (!inDegree.has(e.source) || !inDegree.has(e.target))
|
|
5216
|
+
continue;
|
|
5217
|
+
inDegree.set(e.target, (inDegree.get(e.target) ?? 0) + 1);
|
|
5218
|
+
const list = out.get(e.source);
|
|
5219
|
+
if (list)
|
|
5220
|
+
list.push(e.target);
|
|
5221
|
+
else
|
|
5222
|
+
out.set(e.source, [e.target]);
|
|
5259
5223
|
}
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5224
|
+
const queue = nodes.filter((n) => (inDegree.get(n.id) ?? 0) === 0).map((n) => n.id);
|
|
5225
|
+
let visited = 0;
|
|
5226
|
+
while (queue.length) {
|
|
5227
|
+
const cur = queue.shift();
|
|
5228
|
+
visited++;
|
|
5229
|
+
for (const next of out.get(cur) ?? []) {
|
|
5230
|
+
const d = (inDegree.get(next) ?? 1) - 1;
|
|
5231
|
+
inDegree.set(next, d);
|
|
5232
|
+
if (d === 0)
|
|
5233
|
+
queue.push(next);
|
|
5234
|
+
}
|
|
5264
5235
|
}
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
if (!flow || typeof flow !== "object")
|
|
5277
|
-
return false;
|
|
5278
|
-
const f = flow;
|
|
5279
|
-
return typeof f.id === "string" && typeof f.name === "string" && f.workflow !== undefined && typeof f.workflow === "object" && f.workflow !== null && Array.isArray(f.workflow.steps);
|
|
5280
|
-
}
|
|
5281
|
-
// src/catalog/actions/console-log/config.ts
|
|
5282
|
-
var ConsoleLogConfig = {
|
|
5283
|
-
defaults: {
|
|
5284
|
-
message: "",
|
|
5285
|
-
level: "info",
|
|
5286
|
-
data: undefined,
|
|
5287
|
-
includeInputData: true
|
|
5288
|
-
},
|
|
5289
|
-
schema: {
|
|
5290
|
-
includeInputData: { type: "boolean", required: false },
|
|
5291
|
-
message: {
|
|
5292
|
-
type: "string",
|
|
5293
|
-
required: true,
|
|
5294
|
-
description: "Message to log"
|
|
5295
|
-
},
|
|
5296
|
-
level: {
|
|
5297
|
-
type: "select",
|
|
5298
|
-
required: false,
|
|
5299
|
-
options: ["info", "warning", "error", "debug"],
|
|
5300
|
-
description: "Log level for the message"
|
|
5301
|
-
},
|
|
5302
|
-
data: {
|
|
5303
|
-
type: "json",
|
|
5304
|
-
required: false,
|
|
5305
|
-
description: "Additional data to log"
|
|
5236
|
+
if (visited < nodes.length) {
|
|
5237
|
+
const stuck = nodes.filter((n) => (inDegree.get(n.id) ?? 0) > 0);
|
|
5238
|
+
const names = stuck.slice(0, 3).map(nodeName).join(", ");
|
|
5239
|
+
const more = stuck.length > 3 ? ` +${stuck.length - 3} more` : "";
|
|
5240
|
+
allErrors.push({
|
|
5241
|
+
id: "workflow-cycle",
|
|
5242
|
+
type: "workflow",
|
|
5243
|
+
severity: "error",
|
|
5244
|
+
message: `Circular connection detected involving: ${names}${more}. Break the loop — execution order is undefined inside a cycle.`,
|
|
5245
|
+
nodeId: stuck[0]?.id
|
|
5246
|
+
});
|
|
5306
5247
|
}
|
|
5307
|
-
}
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5248
|
+
}
|
|
5249
|
+
const isTrigger = (n) => n.type === "trigger" || String(n.actionDefinitionId ?? "").endsWith("_trigger");
|
|
5250
|
+
const triggerIds = nodes.filter(isTrigger).map((n) => n.id);
|
|
5251
|
+
const adjacency = new Map;
|
|
5252
|
+
for (const e of edges) {
|
|
5253
|
+
const list = adjacency.get(e.source);
|
|
5254
|
+
if (list)
|
|
5255
|
+
list.push(e.target);
|
|
5256
|
+
else
|
|
5257
|
+
adjacency.set(e.source, [e.target]);
|
|
5258
|
+
}
|
|
5259
|
+
const reachable = new Set(triggerIds);
|
|
5260
|
+
const queue = [...triggerIds];
|
|
5261
|
+
while (queue.length) {
|
|
5262
|
+
const cur = queue.shift();
|
|
5263
|
+
for (const next of adjacency.get(cur) ?? []) {
|
|
5264
|
+
if (!reachable.has(next)) {
|
|
5265
|
+
reachable.add(next);
|
|
5266
|
+
queue.push(next);
|
|
5267
|
+
}
|
|
5314
5268
|
}
|
|
5315
|
-
|
|
5316
|
-
|
|
5269
|
+
}
|
|
5270
|
+
let changed = true;
|
|
5271
|
+
while (changed) {
|
|
5272
|
+
changed = false;
|
|
5273
|
+
for (const n of nodes) {
|
|
5274
|
+
if (reachable.has(n.id)) {
|
|
5275
|
+
if (n.parentId && !reachable.has(n.parentId)) {
|
|
5276
|
+
reachable.add(n.parentId);
|
|
5277
|
+
changed = true;
|
|
5278
|
+
}
|
|
5279
|
+
} else if (n.parentId && reachable.has(n.parentId)) {
|
|
5280
|
+
reachable.add(n.id);
|
|
5281
|
+
changed = true;
|
|
5282
|
+
}
|
|
5317
5283
|
}
|
|
5318
|
-
return {
|
|
5319
|
-
valid: errors.length === 0,
|
|
5320
|
-
errors
|
|
5321
|
-
};
|
|
5322
|
-
},
|
|
5323
|
-
merge: (userConfig) => {
|
|
5324
|
-
return {
|
|
5325
|
-
...ConsoleLogConfig.defaults,
|
|
5326
|
-
...userConfig
|
|
5327
|
-
};
|
|
5328
|
-
},
|
|
5329
|
-
getLabel: (config) => {
|
|
5330
|
-
return `Log [${config.level}]: ${config.message}`;
|
|
5331
5284
|
}
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5285
|
+
nodes.forEach((node) => {
|
|
5286
|
+
if (isTrigger(node))
|
|
5287
|
+
return;
|
|
5288
|
+
const isConnected = edges.some((e) => e.source === node.id || e.target === node.id);
|
|
5289
|
+
const wontRun = triggerIds.length > 0 ? !reachable.has(node.id) : !isConnected;
|
|
5290
|
+
if (wontRun) {
|
|
5291
|
+
allErrors.push({
|
|
5292
|
+
id: `node-${node.id}-orphan`,
|
|
5293
|
+
type: "node",
|
|
5294
|
+
severity: "warning",
|
|
5295
|
+
message: isConnected ? `"${nodeName(node)}" isn't reachable from a trigger and won't run.` : `"${nodeName(node)}" isn't connected to a flow and won't run.`,
|
|
5296
|
+
nodeId: node.id
|
|
5297
|
+
});
|
|
5298
|
+
}
|
|
5299
|
+
});
|
|
5300
|
+
if (triggerIds.length > 1) {
|
|
5301
|
+
const parentOf = new Map(nodes.filter((n) => n.parentId).map((n) => [n.id, n.parentId]));
|
|
5302
|
+
const undirected = new Map;
|
|
5303
|
+
const link = (a, b) => {
|
|
5304
|
+
const la = undirected.get(a);
|
|
5305
|
+
if (la)
|
|
5306
|
+
la.push(b);
|
|
5307
|
+
else
|
|
5308
|
+
undirected.set(a, [b]);
|
|
5309
|
+
};
|
|
5310
|
+
for (const e of edges) {
|
|
5311
|
+
link(e.source, e.target);
|
|
5312
|
+
link(e.target, e.source);
|
|
5313
|
+
}
|
|
5314
|
+
for (const [child, parent] of parentOf) {
|
|
5315
|
+
link(child, parent);
|
|
5316
|
+
link(parent, child);
|
|
5317
|
+
}
|
|
5318
|
+
const seen = new Set;
|
|
5319
|
+
let flowsWithTrigger = 0;
|
|
5320
|
+
for (const t of triggerIds) {
|
|
5321
|
+
if (seen.has(t))
|
|
5322
|
+
continue;
|
|
5323
|
+
flowsWithTrigger++;
|
|
5324
|
+
const stack = [t];
|
|
5325
|
+
seen.add(t);
|
|
5326
|
+
while (stack.length) {
|
|
5327
|
+
const cur = stack.pop();
|
|
5328
|
+
for (const next of undirected.get(cur) ?? []) {
|
|
5329
|
+
if (!seen.has(next)) {
|
|
5330
|
+
seen.add(next);
|
|
5331
|
+
stack.push(next);
|
|
5332
|
+
}
|
|
5333
|
+
}
|
|
5334
|
+
}
|
|
5352
5335
|
}
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
} else if (config.duration < 0) {
|
|
5361
|
-
errors.push("Duration must be non-negative");
|
|
5336
|
+
if (flowsWithTrigger > 1) {
|
|
5337
|
+
allErrors.push({
|
|
5338
|
+
id: "workflow-multi-flow",
|
|
5339
|
+
type: "workflow",
|
|
5340
|
+
severity: "warning",
|
|
5341
|
+
message: `This canvas holds ${flowsWithTrigger} separate flows. Each trigger runs only its own flow — consider splitting them into separate flows for clarity.`
|
|
5342
|
+
});
|
|
5362
5343
|
}
|
|
5363
|
-
|
|
5364
|
-
|
|
5344
|
+
}
|
|
5345
|
+
const errors = allErrors.filter((e) => e.severity === "error");
|
|
5346
|
+
const warnings = allErrors.filter((e) => e.severity === "warning");
|
|
5347
|
+
return {
|
|
5348
|
+
isValid: errors.length === 0,
|
|
5349
|
+
errors,
|
|
5350
|
+
warnings
|
|
5351
|
+
};
|
|
5352
|
+
}
|
|
5353
|
+
function getNodeValidationStatus(nodeId, validationResult) {
|
|
5354
|
+
const hasError = validationResult.errors.some((e) => e.nodeId === nodeId);
|
|
5355
|
+
if (hasError)
|
|
5356
|
+
return "error";
|
|
5357
|
+
const hasWarning = validationResult.warnings.some((e) => e.nodeId === nodeId);
|
|
5358
|
+
if (hasWarning)
|
|
5359
|
+
return "warning";
|
|
5360
|
+
return "valid";
|
|
5361
|
+
}
|
|
5362
|
+
function getEdgeValidationStatus(edgeId, validationResult) {
|
|
5363
|
+
const hasError = validationResult.errors.some((e) => e.edgeId === edgeId);
|
|
5364
|
+
if (hasError)
|
|
5365
|
+
return "error";
|
|
5366
|
+
const hasWarning = validationResult.warnings.some((e) => e.edgeId === edgeId);
|
|
5367
|
+
if (hasWarning)
|
|
5368
|
+
return "warning";
|
|
5369
|
+
return "valid";
|
|
5370
|
+
}
|
|
5371
|
+
// src/catalog/logic/for-each/pathOptions.ts
|
|
5372
|
+
function inferSchemaFromSample(sample) {
|
|
5373
|
+
const properties = {};
|
|
5374
|
+
for (const [key, value] of Object.entries(sample)) {
|
|
5375
|
+
if (value === null || value === undefined) {
|
|
5376
|
+
properties[key] = { type: "string" };
|
|
5377
|
+
} else if (Array.isArray(value)) {
|
|
5378
|
+
if (value.length > 0 && typeof value[0] === "object" && value[0] !== null) {
|
|
5379
|
+
properties[key] = { type: "array", items: inferSchemaFromSample(value[0]) };
|
|
5380
|
+
} else {
|
|
5381
|
+
properties[key] = { type: "array" };
|
|
5382
|
+
}
|
|
5383
|
+
} else if (typeof value === "object") {
|
|
5384
|
+
properties[key] = inferSchemaFromSample(value);
|
|
5385
|
+
} else if (typeof value === "number") {
|
|
5386
|
+
properties[key] = { type: "number" };
|
|
5387
|
+
} else if (typeof value === "boolean") {
|
|
5388
|
+
properties[key] = { type: "boolean" };
|
|
5389
|
+
} else {
|
|
5390
|
+
properties[key] = { type: "string" };
|
|
5365
5391
|
}
|
|
5366
|
-
return {
|
|
5367
|
-
valid: errors.length === 0,
|
|
5368
|
-
errors
|
|
5369
|
-
};
|
|
5370
|
-
},
|
|
5371
|
-
toMilliseconds: (duration, unit = "milliseconds") => {
|
|
5372
|
-
const multipliers = {
|
|
5373
|
-
milliseconds: 1,
|
|
5374
|
-
seconds: 1000,
|
|
5375
|
-
minutes: 60000,
|
|
5376
|
-
hours: 3600000
|
|
5377
|
-
};
|
|
5378
|
-
return duration * (multipliers[unit] || 1);
|
|
5379
|
-
},
|
|
5380
|
-
merge: (userConfig) => {
|
|
5381
|
-
return {
|
|
5382
|
-
...DelayConfig.defaults,
|
|
5383
|
-
...userConfig
|
|
5384
|
-
};
|
|
5385
|
-
},
|
|
5386
|
-
getLabel: (config) => {
|
|
5387
|
-
return `Delay ${config.duration}${config.unit ? " " + config.unit : "ms"}`;
|
|
5388
5392
|
}
|
|
5389
|
-
};
|
|
5390
|
-
|
|
5391
|
-
// src/catalog/actions/http-request/config.ts
|
|
5392
|
-
function urlCheckedAtRunTime(url, connectionId) {
|
|
5393
|
-
const u = url.trim();
|
|
5394
|
-
if (u.startsWith("{{"))
|
|
5395
|
-
return true;
|
|
5396
|
-
const hasScheme = /^[a-z][a-z0-9+.-]*:/i.test(u);
|
|
5397
|
-
return !!connectionId && !hasScheme;
|
|
5393
|
+
return { type: "object", properties };
|
|
5398
5394
|
}
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
body: undefined,
|
|
5408
|
-
formData: [],
|
|
5409
|
-
graphqlQuery: "",
|
|
5410
|
-
graphqlVariables: {},
|
|
5411
|
-
auth: { type: "none" },
|
|
5412
|
-
timeout: 30000,
|
|
5413
|
-
failOnError: true,
|
|
5414
|
-
followRedirects: true,
|
|
5415
|
-
retry: { maxAttempts: 1, retryOn: [429, 500, 502, 503, 504], initialDelayMs: 1000, backoffFactor: 2, maxDelayMs: 30000 },
|
|
5416
|
-
pagination: { type: "none" },
|
|
5417
|
-
extract: {},
|
|
5418
|
-
responseType: "json",
|
|
5419
|
-
expectedContentTypes: []
|
|
5420
|
-
},
|
|
5421
|
-
schema: {
|
|
5422
|
-
apiType: { type: "select", required: false, options: ["rest", "graphql"] },
|
|
5423
|
-
baseUrl: { type: "string", required: false },
|
|
5424
|
-
queryParams: { type: "array", required: false },
|
|
5425
|
-
bodyType: { type: "select", required: false, options: ["none", "json", "raw", "form-data", "xml"] },
|
|
5426
|
-
formData: { type: "array", required: false },
|
|
5427
|
-
graphqlQuery: { type: "code", required: false },
|
|
5428
|
-
graphqlVariables: { type: "json", required: false },
|
|
5429
|
-
auth: { type: "object", required: false },
|
|
5430
|
-
failOnError: { type: "boolean", required: false },
|
|
5431
|
-
followRedirects: { type: "boolean", required: false },
|
|
5432
|
-
retry: { type: "object", required: false },
|
|
5433
|
-
pagination: { type: "object", required: false },
|
|
5434
|
-
extract: { type: "object", required: false },
|
|
5435
|
-
responseType: { type: "select", required: false, options: ["json", "text", "binary"] },
|
|
5436
|
-
expectedContentTypes: { type: "array", required: false },
|
|
5437
|
-
method: {
|
|
5438
|
-
type: "select",
|
|
5439
|
-
required: true,
|
|
5440
|
-
options: ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
|
|
5441
|
-
description: "HTTP method to use"
|
|
5442
|
-
},
|
|
5443
|
-
url: {
|
|
5444
|
-
type: "url",
|
|
5445
|
-
required: true,
|
|
5446
|
-
description: "The URL to send the request to"
|
|
5447
|
-
},
|
|
5448
|
-
headers: {
|
|
5449
|
-
type: "json",
|
|
5450
|
-
required: false,
|
|
5451
|
-
description: "HTTP headers to send with the request"
|
|
5452
|
-
},
|
|
5453
|
-
body: {
|
|
5454
|
-
type: "json",
|
|
5455
|
-
required: false,
|
|
5456
|
-
description: "Request body (for POST, PUT, PATCH)"
|
|
5457
|
-
},
|
|
5458
|
-
timeout: {
|
|
5459
|
-
type: "number",
|
|
5460
|
-
required: false,
|
|
5461
|
-
description: "Request timeout in milliseconds",
|
|
5462
|
-
min: 1000
|
|
5395
|
+
function collectArrayPaths(schema, prefix, add, depth = 0) {
|
|
5396
|
+
if (!schema || typeof schema !== "object" || depth > 8)
|
|
5397
|
+
return;
|
|
5398
|
+
if (schema.type === "array") {
|
|
5399
|
+
if (prefix) {
|
|
5400
|
+
const itemsType = schema.items?.type;
|
|
5401
|
+
const label = itemsType === "object" ? "array of objects" : "array";
|
|
5402
|
+
add(prefix, `${prefix} (${label})`, `Iterate over "${prefix}"`);
|
|
5463
5403
|
}
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
const errors = [];
|
|
5467
|
-
const apiType = config.apiType ?? "rest";
|
|
5468
|
-
if (apiType === "rest" && !config.method) {
|
|
5469
|
-
errors.push("Method is required");
|
|
5404
|
+
if (schema.items) {
|
|
5405
|
+
collectArrayPaths(schema.items, prefix, add, depth + 1);
|
|
5470
5406
|
}
|
|
5471
|
-
|
|
5472
|
-
|
|
5407
|
+
return;
|
|
5408
|
+
}
|
|
5409
|
+
if (schema.type === "object" && schema.properties) {
|
|
5410
|
+
for (const [key, child] of Object.entries(schema.properties)) {
|
|
5411
|
+
const next = prefix ? `${prefix}.${key}` : key;
|
|
5412
|
+
collectArrayPaths(child, next, add, depth + 1);
|
|
5473
5413
|
}
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5414
|
+
}
|
|
5415
|
+
}
|
|
5416
|
+
function collectArrayPathsFromSample(sample, prefix, add, depth = 0) {
|
|
5417
|
+
if (!sample || typeof sample !== "object" || depth > 8)
|
|
5418
|
+
return;
|
|
5419
|
+
if (Array.isArray(sample)) {
|
|
5420
|
+
if (prefix) {
|
|
5421
|
+
const itemsObj = sample.length > 0 && typeof sample[0] === "object" && sample[0] !== null;
|
|
5422
|
+
const label = itemsObj ? "array of objects" : "array";
|
|
5423
|
+
add(prefix, `${prefix} (${label})`, `Iterate over "${prefix}"`);
|
|
5482
5424
|
}
|
|
5483
|
-
if (
|
|
5484
|
-
|
|
5485
|
-
|
|
5425
|
+
if (sample.length > 0) {
|
|
5426
|
+
collectArrayPathsFromSample(sample[0], prefix, add, depth + 1);
|
|
5427
|
+
}
|
|
5428
|
+
return;
|
|
5429
|
+
}
|
|
5430
|
+
for (const [key, value] of Object.entries(sample)) {
|
|
5431
|
+
const next = prefix ? `${prefix}.${key}` : key;
|
|
5432
|
+
collectArrayPathsFromSample(value, next, add, depth + 1);
|
|
5433
|
+
}
|
|
5434
|
+
}
|
|
5435
|
+
function buildPathOptions(upstreamNodes) {
|
|
5436
|
+
const paths = [];
|
|
5437
|
+
const seen = new Set;
|
|
5438
|
+
const add = (id, text, description) => {
|
|
5439
|
+
if (seen.has(id))
|
|
5440
|
+
return;
|
|
5441
|
+
seen.add(id);
|
|
5442
|
+
paths.push({ id, text, description });
|
|
5443
|
+
};
|
|
5444
|
+
if (upstreamNodes) {
|
|
5445
|
+
for (const node of upstreamNodes) {
|
|
5446
|
+
const label = node.label || node.type || "Upstream";
|
|
5447
|
+
const queryTable = node.config?.visualConfig?.tables?.[0];
|
|
5448
|
+
add("data", queryTable ? `${queryTable} (${label})` : `All records (${label})`, queryTable ? `Iterate over every ${queryTable} record` : "Iterate over every record from upstream");
|
|
5449
|
+
const schema = node.outputSchema || node.config?._outputSchema;
|
|
5450
|
+
if (schema)
|
|
5451
|
+
collectArrayPaths(schema, "", add);
|
|
5452
|
+
const output = node.executionData?.output;
|
|
5453
|
+
if (output) {
|
|
5454
|
+
const records = Array.isArray(output.data) ? output.data : Array.isArray(output) ? output : null;
|
|
5455
|
+
if (records && records.length > 0 && typeof records[0] === "object") {
|
|
5456
|
+
collectArrayPathsFromSample(records[0], "", add);
|
|
5457
|
+
}
|
|
5486
5458
|
}
|
|
5487
5459
|
}
|
|
5488
|
-
return {
|
|
5489
|
-
valid: errors.length === 0,
|
|
5490
|
-
errors
|
|
5491
|
-
};
|
|
5492
|
-
},
|
|
5493
|
-
merge: (userConfig) => {
|
|
5494
|
-
return {
|
|
5495
|
-
...HTTPRequestConfig.defaults,
|
|
5496
|
-
...userConfig
|
|
5497
|
-
};
|
|
5498
|
-
},
|
|
5499
|
-
getLabel: (config) => {
|
|
5500
|
-
const type = config.apiType === "graphql" ? "GraphQL" : config.method ?? "GET";
|
|
5501
|
-
return `${type} ${config.url}`;
|
|
5502
5460
|
}
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
// src/catalog/communication/google-chat/config.ts
|
|
5506
|
-
var GoogleChatConfig = {
|
|
5507
|
-
defaults: { connection: "", space: "", spaceName: "", text: "", threadKey: "" },
|
|
5508
|
-
schema: {
|
|
5509
|
-
connection: { type: "connection", required: true },
|
|
5510
|
-
space: { type: "string", required: true },
|
|
5511
|
-
text: { type: "string", required: false },
|
|
5512
|
-
threadKey: { type: "string", required: false }
|
|
5513
|
-
},
|
|
5514
|
-
validate: (config) => {
|
|
5515
|
-
const errors = [];
|
|
5516
|
-
if (!config.connection)
|
|
5517
|
-
errors.push("Connect a Google Chat account");
|
|
5518
|
-
if (!config.space)
|
|
5519
|
-
errors.push("Pick a space");
|
|
5520
|
-
if (!config.text && !config.message)
|
|
5521
|
-
errors.push("Add a message");
|
|
5522
|
-
return { valid: errors.length === 0, errors };
|
|
5523
|
-
},
|
|
5524
|
-
merge: (userConfig) => ({
|
|
5525
|
-
...GoogleChatConfig.defaults,
|
|
5526
|
-
...userConfig
|
|
5527
|
-
}),
|
|
5528
|
-
getLabel: () => "Google Chat"
|
|
5529
|
-
};
|
|
5530
|
-
|
|
5531
|
-
// src/catalog/communication/slack-message/config.ts
|
|
5532
|
-
var SlackMessageConfig = {
|
|
5533
|
-
defaults: { connection: "", messageType: "channel", channel: "", channelName: "", text: "", blocks: undefined },
|
|
5534
|
-
schema: {
|
|
5535
|
-
connection: { type: "connection", required: true },
|
|
5536
|
-
messageType: { type: "string", required: false },
|
|
5537
|
-
channel: { type: "string", required: true },
|
|
5538
|
-
text: { type: "string", required: false },
|
|
5539
|
-
blocks: { type: "json", required: false }
|
|
5540
|
-
},
|
|
5541
|
-
validate: (config) => {
|
|
5542
|
-
const errors = [];
|
|
5543
|
-
if (!config.connection)
|
|
5544
|
-
errors.push("Slack workspace connection is required");
|
|
5545
|
-
if (!config.channel)
|
|
5546
|
-
errors.push(config.messageType === "user" ? "A user is required" : "A channel is required");
|
|
5547
|
-
const hasText = Boolean(config.text || config.message);
|
|
5548
|
-
const hasBlocks = Array.isArray(config.blocks) && config.blocks.length > 0;
|
|
5549
|
-
if (!hasText && !hasBlocks)
|
|
5550
|
-
errors.push("Add a message or at least one block");
|
|
5551
|
-
return { valid: errors.length === 0, errors };
|
|
5552
|
-
},
|
|
5553
|
-
merge: (userConfig) => {
|
|
5554
|
-
return { ...SlackMessageConfig.defaults, ...userConfig };
|
|
5555
|
-
},
|
|
5556
|
-
getLabel: (config) => {
|
|
5557
|
-
const prefix = config.messageType === "user" ? "@" : "#";
|
|
5558
|
-
return config.channelName ? `Slack: ${prefix}${config.channelName}` : config.channel ? `Slack: ${config.channel}` : "Slack";
|
|
5461
|
+
if (paths.length === 0) {
|
|
5462
|
+
add("data", "data (upstream output)");
|
|
5559
5463
|
}
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
// src/catalog/
|
|
5563
|
-
var
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
validate: (config) => {
|
|
5573
|
-
const errors = [];
|
|
5574
|
-
if (!config.connection)
|
|
5575
|
-
errors.push("Connect a Microsoft Teams account");
|
|
5576
|
-
if (!config.teamId)
|
|
5577
|
-
errors.push("Pick a team");
|
|
5578
|
-
if (!config.channelId)
|
|
5579
|
-
errors.push("Pick a channel");
|
|
5580
|
-
if (!config.message)
|
|
5581
|
-
errors.push("Add a message");
|
|
5582
|
-
return { valid: errors.length === 0, errors };
|
|
5583
|
-
},
|
|
5584
|
-
merge: (userConfig) => {
|
|
5585
|
-
return { ...TeamsMessageConfig.defaults, ...userConfig };
|
|
5586
|
-
},
|
|
5587
|
-
getLabel: (config) => config.channelName ? `Teams: #${config.channelName}` : "Teams"
|
|
5588
|
-
};
|
|
5589
|
-
|
|
5590
|
-
// src/catalog/communication/webhook-call/config.ts
|
|
5591
|
-
var WebhookCallConfig = {
|
|
5592
|
-
defaults: {
|
|
5593
|
-
url: "",
|
|
5594
|
-
method: "POST",
|
|
5595
|
-
payload: {},
|
|
5596
|
-
headers: {}
|
|
5597
|
-
},
|
|
5598
|
-
schema: {
|
|
5599
|
-
url: { type: "url", required: true },
|
|
5600
|
-
method: { type: "select", required: true, options: ["POST", "PUT", "PATCH", "GET"] },
|
|
5601
|
-
payload: { type: "json", required: false },
|
|
5602
|
-
headers: { type: "json", required: false }
|
|
5603
|
-
},
|
|
5604
|
-
validate: (config) => {
|
|
5605
|
-
const errors = [];
|
|
5606
|
-
if (!config.url)
|
|
5607
|
-
errors.push("Webhook URL is required");
|
|
5608
|
-
if (!config.method)
|
|
5609
|
-
errors.push("HTTP method is required");
|
|
5610
|
-
return { valid: errors.length === 0, errors };
|
|
5611
|
-
},
|
|
5612
|
-
merge: (userConfig) => {
|
|
5613
|
-
return { ...WebhookCallConfig.defaults, ...userConfig };
|
|
5614
|
-
},
|
|
5615
|
-
getLabel: (config) => {
|
|
5616
|
-
return `${config.method}: ${config.url.substring(0, 40)}...`;
|
|
5464
|
+
return paths;
|
|
5465
|
+
}
|
|
5466
|
+
// src/catalog/actions/delay/migrate.ts
|
|
5467
|
+
var LEGACY_UNITS = new Set([
|
|
5468
|
+
"milliseconds",
|
|
5469
|
+
"seconds",
|
|
5470
|
+
"minutes",
|
|
5471
|
+
"hours"
|
|
5472
|
+
]);
|
|
5473
|
+
function migrateDelayConfig(raw) {
|
|
5474
|
+
if (raw && typeof raw === "object" && "mode" in raw) {
|
|
5475
|
+
return raw;
|
|
5617
5476
|
}
|
|
5618
|
-
};
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5477
|
+
const legacy = raw ?? {};
|
|
5478
|
+
const duration = typeof legacy.duration === "number" && Number.isFinite(legacy.duration) ? legacy.duration : 1;
|
|
5479
|
+
const unit = typeof legacy.unit === "string" && LEGACY_UNITS.has(legacy.unit) ? legacy.unit : "milliseconds";
|
|
5480
|
+
return {
|
|
5481
|
+
mode: "fixed",
|
|
5482
|
+
fixed: { duration, unit }
|
|
5483
|
+
};
|
|
5484
|
+
}
|
|
5485
|
+
// src/catalog/logic/for-each/recordSchema.ts
|
|
5486
|
+
function resolveSchemaPath(root, path) {
|
|
5487
|
+
if (!root)
|
|
5488
|
+
return;
|
|
5489
|
+
const parts = path.split(".").filter(Boolean);
|
|
5490
|
+
let cur = root;
|
|
5491
|
+
for (const part of parts) {
|
|
5492
|
+
if (cur?.type === "object" && cur.properties && part in cur.properties) {
|
|
5493
|
+
cur = cur.properties[part];
|
|
5494
|
+
continue;
|
|
5495
|
+
}
|
|
5496
|
+
if (cur?.type === "array" && cur.items?.properties && part in cur.items.properties) {
|
|
5497
|
+
cur = cur.items.properties[part];
|
|
5498
|
+
continue;
|
|
5499
|
+
}
|
|
5500
|
+
return;
|
|
5501
|
+
}
|
|
5502
|
+
if (cur?.type === "array" && cur.items)
|
|
5503
|
+
return cur.items;
|
|
5504
|
+
if (cur?.type === "object" && cur.properties)
|
|
5505
|
+
return cur;
|
|
5506
|
+
return;
|
|
5507
|
+
}
|
|
5508
|
+
function resolveSamplePath(sample, path) {
|
|
5509
|
+
const parts = path.split(".").filter(Boolean);
|
|
5510
|
+
let cur = sample;
|
|
5511
|
+
for (const part of parts) {
|
|
5512
|
+
if (cur === null || cur === undefined)
|
|
5513
|
+
return;
|
|
5514
|
+
if (Array.isArray(cur)) {
|
|
5515
|
+
if (cur.length === 0 || typeof cur[0] !== "object" || cur[0] === null)
|
|
5516
|
+
return;
|
|
5517
|
+
cur = cur[0][part];
|
|
5518
|
+
continue;
|
|
5519
|
+
}
|
|
5520
|
+
if (typeof cur === "object") {
|
|
5521
|
+
cur = cur[part];
|
|
5522
|
+
continue;
|
|
5523
|
+
}
|
|
5524
|
+
return;
|
|
5525
|
+
}
|
|
5526
|
+
return cur;
|
|
5527
|
+
}
|
|
5528
|
+
function sampleRecords(node) {
|
|
5529
|
+
const output = node?.executionData?.output;
|
|
5530
|
+
if (!output)
|
|
5531
|
+
return null;
|
|
5532
|
+
if (Array.isArray(output))
|
|
5533
|
+
return output;
|
|
5534
|
+
const data = output.data;
|
|
5535
|
+
return Array.isArray(data) ? data : null;
|
|
5536
|
+
}
|
|
5537
|
+
var isRootPath = (path) => path === "" || path === "data";
|
|
5538
|
+
function resolveRecordSchema(upstreamNodes, arrayPath) {
|
|
5539
|
+
const node = upstreamNodes?.[0];
|
|
5540
|
+
if (!node)
|
|
5541
|
+
return;
|
|
5542
|
+
const path = (arrayPath || "").trim();
|
|
5543
|
+
const isRoot = isRootPath(path);
|
|
5544
|
+
const schema = node.outputSchema || node.config?._outputSchema;
|
|
5545
|
+
if (schema?.properties) {
|
|
5546
|
+
if (isRoot) {
|
|
5547
|
+
const hasDeepFields = Object.values(schema.properties).some((f) => f?.properties || f?.items);
|
|
5548
|
+
if (hasDeepFields)
|
|
5549
|
+
return schema;
|
|
5648
5550
|
} else {
|
|
5649
|
-
|
|
5650
|
-
|
|
5551
|
+
const resolved = resolveSchemaPath(schema, path);
|
|
5552
|
+
if (resolved)
|
|
5553
|
+
return resolved;
|
|
5554
|
+
}
|
|
5555
|
+
}
|
|
5556
|
+
const records = sampleRecords(node);
|
|
5557
|
+
if (records && records.length > 0 && typeof records[0] === "object" && records[0] !== null) {
|
|
5558
|
+
const sample = records[0];
|
|
5559
|
+
if (isRoot)
|
|
5560
|
+
return inferSchemaFromSample(sample);
|
|
5561
|
+
const resolved = resolveSamplePath(sample, path);
|
|
5562
|
+
if (Array.isArray(resolved) && resolved.length > 0 && typeof resolved[0] === "object" && resolved[0] !== null) {
|
|
5563
|
+
return inferSchemaFromSample(resolved[0]);
|
|
5651
5564
|
}
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
merge: (userConfig) => {
|
|
5655
|
-
return { ...WhatsappSendConfig.defaults, ...userConfig };
|
|
5656
|
-
},
|
|
5657
|
-
getLabel: (config) => config.mode === "text" ? `WhatsApp to ${config.to || "—"}` : `WhatsApp template "${config.templateName || "—"}" to ${config.to || "—"}`
|
|
5658
|
-
};
|
|
5659
|
-
|
|
5660
|
-
// src/catalog/triggers/schedule-trigger/config.ts
|
|
5661
|
-
var ScheduleTriggerConfig = {
|
|
5662
|
-
defaults: {
|
|
5663
|
-
scheduleType: "interval",
|
|
5664
|
-
interval: 60,
|
|
5665
|
-
intervalUnit: "minutes",
|
|
5666
|
-
timezone: "UTC"
|
|
5667
|
-
},
|
|
5668
|
-
schema: {
|
|
5669
|
-
scheduleType: { type: "select", required: true, options: ["interval", "cron", "specific"] },
|
|
5670
|
-
interval: { type: "number", required: false, min: 1 },
|
|
5671
|
-
intervalUnit: { type: "select", required: false, options: ["minutes", "hours", "days", "weeks"] },
|
|
5672
|
-
cronExpression: { type: "string", required: false },
|
|
5673
|
-
timezone: { type: "string", required: false }
|
|
5674
|
-
},
|
|
5675
|
-
validate: (config) => {
|
|
5676
|
-
const errors = [];
|
|
5677
|
-
if (!config.scheduleType) {
|
|
5678
|
-
errors.push("Schedule type is required");
|
|
5565
|
+
if (resolved && typeof resolved === "object" && !Array.isArray(resolved)) {
|
|
5566
|
+
return inferSchemaFromSample(resolved);
|
|
5679
5567
|
}
|
|
5680
|
-
if (
|
|
5681
|
-
|
|
5682
|
-
|
|
5568
|
+
if (!path.includes("."))
|
|
5569
|
+
return inferSchemaFromSample(sample);
|
|
5570
|
+
}
|
|
5571
|
+
return schema?.properties ? schema : undefined;
|
|
5572
|
+
}
|
|
5573
|
+
function detectRecordFields(upstreamNodes, arrayPath) {
|
|
5574
|
+
const set = new Set;
|
|
5575
|
+
const props = resolveRecordSchema(upstreamNodes, arrayPath)?.properties;
|
|
5576
|
+
if (props && typeof props === "object")
|
|
5577
|
+
for (const k of Object.keys(props))
|
|
5578
|
+
set.add(k);
|
|
5579
|
+
const records = sampleRecords(upstreamNodes?.[0]);
|
|
5580
|
+
if (records) {
|
|
5581
|
+
const path = (arrayPath || "").trim();
|
|
5582
|
+
const addKeys = (v) => {
|
|
5583
|
+
if (v && typeof v === "object" && !Array.isArray(v))
|
|
5584
|
+
for (const k of Object.keys(v))
|
|
5585
|
+
set.add(k);
|
|
5586
|
+
};
|
|
5587
|
+
for (const r of records.slice(0, 25)) {
|
|
5588
|
+
if (isRootPath(path)) {
|
|
5589
|
+
addKeys(r);
|
|
5590
|
+
continue;
|
|
5683
5591
|
}
|
|
5592
|
+
const at = resolveSamplePath(r, path);
|
|
5593
|
+
if (Array.isArray(at))
|
|
5594
|
+
for (const item of at.slice(0, 25))
|
|
5595
|
+
addKeys(item);
|
|
5596
|
+
else if (at && typeof at === "object")
|
|
5597
|
+
addKeys(at);
|
|
5598
|
+
else if (!path.includes("."))
|
|
5599
|
+
addKeys(r);
|
|
5684
5600
|
}
|
|
5685
|
-
|
|
5686
|
-
|
|
5601
|
+
}
|
|
5602
|
+
return Array.from(set);
|
|
5603
|
+
}
|
|
5604
|
+
// src/catalog/workflow.ts
|
|
5605
|
+
var NodeStatus;
|
|
5606
|
+
((NodeStatus) => {
|
|
5607
|
+
NodeStatus["Idle"] = "idle";
|
|
5608
|
+
NodeStatus["Running"] = "running";
|
|
5609
|
+
NodeStatus["Success"] = "success";
|
|
5610
|
+
NodeStatus["Error"] = "error";
|
|
5611
|
+
NodeStatus["Warning"] = "warning";
|
|
5612
|
+
NodeStatus["Disabled"] = "disabled";
|
|
5613
|
+
})(NodeStatus ||= {});
|
|
5614
|
+
var EdgeType;
|
|
5615
|
+
((EdgeType) => {
|
|
5616
|
+
EdgeType["Default"] = "default";
|
|
5617
|
+
EdgeType["Conditional"] = "conditional";
|
|
5618
|
+
EdgeType["Loop"] = "loop";
|
|
5619
|
+
EdgeType["Error"] = "error";
|
|
5620
|
+
})(EdgeType ||= {});
|
|
5621
|
+
// src/rules/flattenNestedWorkflow.ts
|
|
5622
|
+
function workflowIsNested(workflow) {
|
|
5623
|
+
const stack = [...workflow.steps];
|
|
5624
|
+
while (stack.length > 0) {
|
|
5625
|
+
const s = stack.pop();
|
|
5626
|
+
if (s.branches && Object.keys(s.branches).length > 0)
|
|
5627
|
+
return true;
|
|
5628
|
+
}
|
|
5629
|
+
return false;
|
|
5630
|
+
}
|
|
5631
|
+
function flattenNestedWorkflow(workflow) {
|
|
5632
|
+
if (!workflowIsNested(workflow))
|
|
5633
|
+
return workflow;
|
|
5634
|
+
const flatSteps = [];
|
|
5635
|
+
const flatConns = [...workflow.connections];
|
|
5636
|
+
const groups = [];
|
|
5637
|
+
const walk = (steps, parentIdForSubflow) => {
|
|
5638
|
+
for (const step of steps) {
|
|
5639
|
+
const { branches, ...rest } = step;
|
|
5640
|
+
const flatStep = {
|
|
5641
|
+
...rest,
|
|
5642
|
+
...parentIdForSubflow ? { parentId: parentIdForSubflow } : {}
|
|
5643
|
+
};
|
|
5644
|
+
flatSteps.push(flatStep);
|
|
5645
|
+
if (!branches)
|
|
5646
|
+
continue;
|
|
5647
|
+
if (branches.body && step.type === "group") {
|
|
5648
|
+
const body = branches.body;
|
|
5649
|
+
groups.push({
|
|
5650
|
+
group: flatStep,
|
|
5651
|
+
bodyStepIds: body.steps.map((s) => s.id),
|
|
5652
|
+
bodyConnections: body.connections
|
|
5653
|
+
});
|
|
5654
|
+
for (const c of body.connections)
|
|
5655
|
+
flatConns.push(c);
|
|
5656
|
+
walk(body.steps, step.id);
|
|
5657
|
+
}
|
|
5658
|
+
for (const branchKey of Object.keys(branches)) {
|
|
5659
|
+
if (branchKey === "body" && step.type === "group")
|
|
5660
|
+
continue;
|
|
5661
|
+
const sub = branches[branchKey];
|
|
5662
|
+
if (!sub)
|
|
5663
|
+
continue;
|
|
5664
|
+
for (const c of sub.connections)
|
|
5665
|
+
flatConns.push(c);
|
|
5666
|
+
walk(sub.steps, parentIdForSubflow);
|
|
5667
|
+
}
|
|
5687
5668
|
}
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5669
|
+
};
|
|
5670
|
+
walk(workflow.steps, undefined);
|
|
5671
|
+
for (const g of groups) {
|
|
5672
|
+
if (g.bodyStepIds.length === 0)
|
|
5673
|
+
continue;
|
|
5674
|
+
const bodyIdSet = new Set(g.bodyStepIds);
|
|
5675
|
+
const inDeg = new Map;
|
|
5676
|
+
const outDeg = new Map;
|
|
5677
|
+
for (const id of g.bodyStepIds) {
|
|
5678
|
+
inDeg.set(id, 0);
|
|
5679
|
+
outDeg.set(id, 0);
|
|
5696
5680
|
}
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
method: "POST",
|
|
5705
|
-
requireAuth: true
|
|
5706
|
-
},
|
|
5707
|
-
schema: {
|
|
5708
|
-
webhookPath: { type: "string", required: false },
|
|
5709
|
-
method: { type: "select", required: true, options: ["GET", "POST", "PUT", "PATCH", "DELETE"] },
|
|
5710
|
-
requireAuth: { type: "boolean", required: false }
|
|
5711
|
-
},
|
|
5712
|
-
validate: (config) => {
|
|
5713
|
-
const errors = [];
|
|
5714
|
-
if (!config.method) {
|
|
5715
|
-
errors.push("HTTP method is required");
|
|
5681
|
+
for (const c of g.bodyConnections) {
|
|
5682
|
+
if (bodyIdSet.has(c.sourceStepId)) {
|
|
5683
|
+
outDeg.set(c.sourceStepId, (outDeg.get(c.sourceStepId) ?? 0) + 1);
|
|
5684
|
+
}
|
|
5685
|
+
if (bodyIdSet.has(c.targetStepId)) {
|
|
5686
|
+
inDeg.set(c.targetStepId, (inDeg.get(c.targetStepId) ?? 0) + 1);
|
|
5687
|
+
}
|
|
5716
5688
|
}
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
};
|
|
5726
|
-
|
|
5727
|
-
// src/rules/configContracts.ts
|
|
5728
|
-
var asValidator = (fn) => (config) => fn(config);
|
|
5729
|
-
var CONFIG_CONTRACTS = {
|
|
5730
|
-
console_log: { schema: ConsoleLogConfig.schema, validate: asValidator(ConsoleLogConfig.validate) },
|
|
5731
|
-
delay: { schema: DelayConfig.schema, validate: asValidator(DelayConfig.validate) },
|
|
5732
|
-
http_request: { schema: HTTPRequestConfig.schema, validate: asValidator(HTTPRequestConfig.validate) },
|
|
5733
|
-
email: { schema: EmailConfig.schema, validate: asValidator(EmailConfig.validate) },
|
|
5734
|
-
google_chat: { schema: GoogleChatConfig.schema, validate: asValidator(GoogleChatConfig.validate) },
|
|
5735
|
-
slack_message: { schema: SlackMessageConfig.schema, validate: asValidator(SlackMessageConfig.validate) },
|
|
5736
|
-
teams_message: { schema: TeamsMessageConfig.schema, validate: asValidator(TeamsMessageConfig.validate) },
|
|
5737
|
-
webhook_call: { schema: WebhookCallConfig.schema, validate: asValidator(WebhookCallConfig.validate) },
|
|
5738
|
-
whatsapp_send: { schema: WhatsappSendConfig.schema, validate: asValidator(WhatsappSendConfig.validate) },
|
|
5739
|
-
query: {
|
|
5740
|
-
schema: BuildQueryContractSchema,
|
|
5741
|
-
validate: (config) => {
|
|
5742
|
-
const r = validateBuildQueryConfig(config);
|
|
5743
|
-
const errors = (r.errors ?? []).filter((e) => e.severity !== "warning").map((e) => e.message);
|
|
5744
|
-
return { valid: errors.length === 0, errors };
|
|
5689
|
+
const roots = g.bodyStepIds.filter((id) => (inDeg.get(id) ?? 0) === 0);
|
|
5690
|
+
const leaves = g.bodyStepIds.filter((id) => (outDeg.get(id) ?? 0) === 0);
|
|
5691
|
+
for (const rootId of roots) {
|
|
5692
|
+
flatConns.push({
|
|
5693
|
+
id: `synth-inner-in-${g.group.id}-${rootId}`,
|
|
5694
|
+
sourceStepId: g.group.id,
|
|
5695
|
+
targetStepId: rootId,
|
|
5696
|
+
sourcePort: "_inner_in"
|
|
5697
|
+
});
|
|
5745
5698
|
}
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
ai_agent: { schema: AiAgentConfig.schema, validate: asValidator(AiAgentConfig.validate) },
|
|
5757
|
-
idp_extract: { schema: IdpExtractConfig.schema, validate: asValidator(IdpExtractConfig.validate) },
|
|
5758
|
-
group: { schema: GroupConfig.schema, validate: asValidator(GroupConfig.validate) },
|
|
5759
|
-
connector_trigger: { schema: ConnectorTriggerConfig.schema, validate: asValidator(ConnectorTriggerConfig.validate) },
|
|
5760
|
-
csv_action: { schema: CsvActionConfig.schema, validate: asValidator(CsvActionConfig.validate) },
|
|
5761
|
-
json_action: { schema: JsonActionConfig.schema, validate: asValidator(JsonActionConfig.validate) },
|
|
5762
|
-
xml_action: { schema: XmlActionConfig.schema, validate: asValidator(XmlActionConfig.validate) },
|
|
5763
|
-
excel_action: { schema: ExcelActionConfig.schema, validate: asValidator(ExcelActionConfig.validate) },
|
|
5764
|
-
storage_action: { schema: StorageActionConfig.schema, validate: asValidator(StorageActionConfig.validate) },
|
|
5765
|
-
s3_action: { schema: S3ActionConfig.schema, validate: asValidator(S3ActionConfig.validate) },
|
|
5766
|
-
ftp_action: { schema: FtpActionConfig.schema, validate: asValidator(FtpActionConfig.validate) },
|
|
5767
|
-
google_drive_action: { schema: GoogleDriveActionConfig.schema, validate: asValidator(GoogleDriveActionConfig.validate) },
|
|
5768
|
-
dropbox_action: { schema: DropboxActionConfig.schema, validate: asValidator(DropboxActionConfig.validate) },
|
|
5769
|
-
onedrive_action: { schema: OneDriveActionConfig.schema, validate: asValidator(OneDriveActionConfig.validate) }
|
|
5770
|
-
};
|
|
5771
|
-
function contractFor(actionId) {
|
|
5772
|
-
return actionId ? CONFIG_CONTRACTS[actionId] : undefined;
|
|
5773
|
-
}
|
|
5774
|
-
function declaredKeys(actionId) {
|
|
5775
|
-
const c = contractFor(actionId);
|
|
5776
|
-
if (!c)
|
|
5777
|
-
return [];
|
|
5778
|
-
return Object.entries(c.schema).filter(([, f]) => f.type !== "none").map(([k]) => k);
|
|
5699
|
+
for (const leafId of leaves) {
|
|
5700
|
+
flatConns.push({
|
|
5701
|
+
id: `synth-inner-out-${leafId}-${g.group.id}`,
|
|
5702
|
+
sourceStepId: leafId,
|
|
5703
|
+
targetStepId: g.group.id,
|
|
5704
|
+
targetPort: "_inner_out"
|
|
5705
|
+
});
|
|
5706
|
+
}
|
|
5707
|
+
}
|
|
5708
|
+
return { ...workflow, steps: flatSteps, connections: flatConns };
|
|
5779
5709
|
}
|
|
5780
|
-
|
|
5781
|
-
|
|
5710
|
+
|
|
5711
|
+
// src/rules/flowShape.ts
|
|
5712
|
+
function transformStepToNode(step) {
|
|
5713
|
+
const stepType = step.type;
|
|
5714
|
+
const stepActionDefId = step.actionDefinitionId ?? "";
|
|
5715
|
+
const nodeType = stepType === "trigger" ? "trigger" : stepType === "group" || stepActionDefId === "group" ? "group" : "action";
|
|
5716
|
+
let nodeStatus = "idle" /* Idle */;
|
|
5717
|
+
switch (step.status?.toLowerCase()) {
|
|
5718
|
+
case "running":
|
|
5719
|
+
case "executing":
|
|
5720
|
+
nodeStatus = "running" /* Running */;
|
|
5721
|
+
break;
|
|
5722
|
+
case "completed":
|
|
5723
|
+
case "success":
|
|
5724
|
+
nodeStatus = "success" /* Success */;
|
|
5725
|
+
break;
|
|
5726
|
+
case "failed":
|
|
5727
|
+
case "error":
|
|
5728
|
+
nodeStatus = "error" /* Error */;
|
|
5729
|
+
break;
|
|
5730
|
+
case "pending":
|
|
5731
|
+
case "waiting":
|
|
5732
|
+
nodeStatus = "idle" /* Idle */;
|
|
5733
|
+
break;
|
|
5734
|
+
default:
|
|
5735
|
+
nodeStatus = "idle" /* Idle */;
|
|
5736
|
+
}
|
|
5737
|
+
const position = step.position || { x: step.position_x || 0, y: step.position_y || 0 };
|
|
5738
|
+
const posX = typeof position === "object" && position !== null && "x" in position ? position.x : step.position_x || 0;
|
|
5739
|
+
const posY = typeof position === "object" && position !== null && "y" in position ? position.y : step.position_y || 0;
|
|
5740
|
+
const config = step.config || {};
|
|
5741
|
+
const configData = config.data || config || {};
|
|
5742
|
+
const derivedFromName = step.name ? step.name.toLowerCase().replace(/\s+/g, "_") : undefined;
|
|
5743
|
+
const resolvedActionDefinitionId = step.actionDefinitionId || config.connector_id || step.connector_id || derivedFromName || step.type;
|
|
5744
|
+
const actionDef = getActionById(resolvedActionDefinitionId);
|
|
5745
|
+
const node = {
|
|
5746
|
+
id: step.id,
|
|
5747
|
+
type: nodeType,
|
|
5748
|
+
actionDefinitionId: resolvedActionDefinitionId,
|
|
5749
|
+
actionDefinition: actionDef,
|
|
5750
|
+
label: step.label || step.name,
|
|
5751
|
+
description: step.description || config.description || actionDef?.description || "",
|
|
5752
|
+
icon: actionDef?.icon ?? getIconForStepType(step.type),
|
|
5753
|
+
position: {
|
|
5754
|
+
x: posX,
|
|
5755
|
+
y: posY
|
|
5756
|
+
},
|
|
5757
|
+
...step.parentId ? { parentId: step.parentId } : {},
|
|
5758
|
+
...step.extent ? { extent: step.extent } : {},
|
|
5759
|
+
...step.disabled ? { disabled: true } : {},
|
|
5760
|
+
...step.dimensions && nodeType === "group" ? { dimensions: step.dimensions } : nodeType === "group" ? { dimensions: { width: 480, height: 240 } } : {},
|
|
5761
|
+
config: {
|
|
5762
|
+
...configData,
|
|
5763
|
+
timeout: step.timeout_seconds,
|
|
5764
|
+
retryPolicy: step.retry_policy,
|
|
5765
|
+
notes: step.notes,
|
|
5766
|
+
tags: step.tags,
|
|
5767
|
+
stepCount: step.step_count
|
|
5768
|
+
},
|
|
5769
|
+
ports: {
|
|
5770
|
+
input: nodeType === "action" || nodeType === "group" ? [
|
|
5771
|
+
{
|
|
5772
|
+
id: `${step.id}-input`,
|
|
5773
|
+
type: "input",
|
|
5774
|
+
position: "top",
|
|
5775
|
+
connected: false
|
|
5776
|
+
}
|
|
5777
|
+
] : [],
|
|
5778
|
+
output: (() => {
|
|
5779
|
+
if (resolvedActionDefinitionId === "condition") {
|
|
5780
|
+
return [
|
|
5781
|
+
{ id: `${step.id}_output_true`, type: "output", position: "bottom", label: "True", connected: false },
|
|
5782
|
+
{ id: `${step.id}_output_false`, type: "output", position: "bottom", label: "False", connected: false }
|
|
5783
|
+
];
|
|
5784
|
+
}
|
|
5785
|
+
if (resolvedActionDefinitionId === "for_each" || resolvedActionDefinitionId === "for-each") {
|
|
5786
|
+
return [
|
|
5787
|
+
{ id: `${step.id}_output_each`, type: "output", position: "bottom", label: "Each", connected: false },
|
|
5788
|
+
{ id: `${step.id}_output_done`, type: "output", position: "bottom", label: "Done", connected: false }
|
|
5789
|
+
];
|
|
5790
|
+
}
|
|
5791
|
+
if (resolvedActionDefinitionId === "group") {
|
|
5792
|
+
return [
|
|
5793
|
+
{ id: `${step.id}_output_0`, type: "output", position: "bottom", connected: false }
|
|
5794
|
+
];
|
|
5795
|
+
}
|
|
5796
|
+
return [
|
|
5797
|
+
{ id: `${step.id}-output`, type: "output", position: "bottom", connected: false }
|
|
5798
|
+
];
|
|
5799
|
+
})()
|
|
5800
|
+
},
|
|
5801
|
+
configStatus: step.configStatus || undefined,
|
|
5802
|
+
...step.outputSchema ? { outputSchema: step.outputSchema } : {},
|
|
5803
|
+
status: nodeStatus,
|
|
5804
|
+
selected: false,
|
|
5805
|
+
dragging: false,
|
|
5806
|
+
isValid: step.configStatus ? step.configStatus === "valid" : true,
|
|
5807
|
+
expanded: step.expanded,
|
|
5808
|
+
minimized: false,
|
|
5809
|
+
locked: false,
|
|
5810
|
+
createdAt: step.created_at || new Date().toISOString(),
|
|
5811
|
+
updatedAt: step.updated_at || new Date().toISOString()
|
|
5812
|
+
};
|
|
5813
|
+
return node;
|
|
5782
5814
|
}
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5815
|
+
function transformConnectionToEdge(connection) {
|
|
5816
|
+
const sourceStepId = connection.sourceStepId || connection.source_step_id;
|
|
5817
|
+
const targetStepId = connection.targetStepId || connection.target_step_id;
|
|
5818
|
+
const sourcePort = connection.sourcePort || connection.source_port || `${sourceStepId}-output`;
|
|
5819
|
+
const targetPort = connection.targetPort || connection.target_port || `${targetStepId}-input`;
|
|
5820
|
+
let edgeType = "default" /* Default */;
|
|
5821
|
+
if (connection.type === "success" || connection.type === "failure") {
|
|
5822
|
+
edgeType = "conditional" /* Conditional */;
|
|
5823
|
+
}
|
|
5824
|
+
return {
|
|
5825
|
+
id: connection.id,
|
|
5826
|
+
source: sourceStepId,
|
|
5827
|
+
target: targetStepId,
|
|
5828
|
+
sourcePort,
|
|
5829
|
+
targetPort,
|
|
5830
|
+
type: edgeType,
|
|
5831
|
+
label: connection.label,
|
|
5832
|
+
selected: false,
|
|
5833
|
+
animated: false,
|
|
5834
|
+
createdAt: new Date().toISOString()
|
|
5835
|
+
};
|
|
5799
5836
|
}
|
|
5800
|
-
function
|
|
5801
|
-
const
|
|
5802
|
-
const
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
type: CONTRACT_TYPE_TO_FIELD_TYPE[field.type] ?? "string" /* String */,
|
|
5814
|
-
validation: { required: field.required === true },
|
|
5815
|
-
...typeof field.description === "string" && field.description ? { description: field.description } : {},
|
|
5816
|
-
...field.options ? { options: field.options.map((v) => ({ label: String(v), value: v })) } : {}
|
|
5837
|
+
function generateConnectionsFromSteps(steps) {
|
|
5838
|
+
const connections = [];
|
|
5839
|
+
const sortedSteps = [...steps].sort((a, b) => a.step_count - b.step_count);
|
|
5840
|
+
for (let i = 0;i < sortedSteps.length - 1; i++) {
|
|
5841
|
+
const sourceStep = sortedSteps[i];
|
|
5842
|
+
const targetStep = sortedSteps[i + 1];
|
|
5843
|
+
connections.push({
|
|
5844
|
+
id: `${sourceStep.id}-${targetStep.id}`,
|
|
5845
|
+
source_step_id: sourceStep.id,
|
|
5846
|
+
target_step_id: targetStep.id,
|
|
5847
|
+
source_port: `${sourceStep.id}-output`,
|
|
5848
|
+
target_port: `${targetStep.id}-input`,
|
|
5849
|
+
type: "default"
|
|
5817
5850
|
});
|
|
5818
5851
|
}
|
|
5819
|
-
return
|
|
5820
|
-
}
|
|
5821
|
-
function selectsWithoutValues(actionId) {
|
|
5822
|
-
const contract = contractFor(actionId);
|
|
5823
|
-
if (!contract)
|
|
5824
|
-
return [];
|
|
5825
|
-
return Object.entries(contract.schema).filter(([, f]) => f.type === "select" && !(f.options && f.options.length > 0) && f.dynamic !== true).map(([key]) => key);
|
|
5852
|
+
return connections;
|
|
5826
5853
|
}
|
|
5827
|
-
function
|
|
5828
|
-
const
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
const root = key.split(".")[0];
|
|
5835
|
-
const rootField = contract.schema[root];
|
|
5836
|
-
return !!rootField && (rootField.type === "object" || rootField.type === "json");
|
|
5854
|
+
function getIconForStepType(type) {
|
|
5855
|
+
const iconMap = {
|
|
5856
|
+
trigger: "PlayOutline",
|
|
5857
|
+
transform: "Transform",
|
|
5858
|
+
action: "Application",
|
|
5859
|
+
condition: "DecisionTree",
|
|
5860
|
+
loop: "Repeat"
|
|
5837
5861
|
};
|
|
5838
|
-
return
|
|
5862
|
+
return iconMap[type] || "Application";
|
|
5839
5863
|
}
|
|
5840
|
-
function
|
|
5841
|
-
const
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5864
|
+
function transformFlowToWorkflow(flow) {
|
|
5865
|
+
const flatWorkflow = flattenNestedWorkflow(flow.workflow);
|
|
5866
|
+
const nodes = flatWorkflow.steps.map((step) => transformStepToNode(step));
|
|
5867
|
+
let connections = flatWorkflow.connections;
|
|
5868
|
+
if (connections === undefined || connections === null) {
|
|
5869
|
+
connections = generateConnectionsFromSteps(flatWorkflow.steps);
|
|
5870
|
+
} else {
|
|
5871
|
+
connections = connections || [];
|
|
5872
|
+
}
|
|
5873
|
+
const edges = connections.map((conn, index) => {
|
|
5874
|
+
if (!conn.id) {
|
|
5875
|
+
conn.id = `edge-${conn.source_step_id}-${conn.target_step_id}-${index}`;
|
|
5876
|
+
}
|
|
5877
|
+
return transformConnectionToEdge(conn);
|
|
5878
|
+
});
|
|
5879
|
+
edges.forEach((edge) => {
|
|
5880
|
+
const sourceNode = nodes.find((n) => n.id === edge.source);
|
|
5881
|
+
const targetNode = nodes.find((n) => n.id === edge.target);
|
|
5882
|
+
if (sourceNode) {
|
|
5883
|
+
const port = sourceNode.ports.output.find((p) => p.id === edge.sourcePort);
|
|
5884
|
+
if (port)
|
|
5885
|
+
port.connected = true;
|
|
5886
|
+
}
|
|
5887
|
+
if (targetNode) {
|
|
5888
|
+
const port = targetNode.ports.input.find((p) => p.id === edge.targetPort);
|
|
5889
|
+
if (port)
|
|
5890
|
+
port.connected = true;
|
|
5891
|
+
}
|
|
5892
|
+
});
|
|
5893
|
+
return {
|
|
5894
|
+
id: flow.workflow.id,
|
|
5895
|
+
name: flow.workflow.name,
|
|
5896
|
+
description: flow.workflow.description,
|
|
5897
|
+
nodes,
|
|
5898
|
+
edges,
|
|
5899
|
+
workflowHistory: flow.workflowHistory || []
|
|
5900
|
+
};
|
|
5846
5901
|
}
|
|
5847
|
-
function
|
|
5848
|
-
|
|
5849
|
-
|
|
5902
|
+
function validateFlowData(flow) {
|
|
5903
|
+
if (!flow || typeof flow !== "object")
|
|
5904
|
+
return false;
|
|
5905
|
+
const f = flow;
|
|
5906
|
+
return typeof f.id === "string" && typeof f.name === "string" && f.workflow !== undefined && typeof f.workflow === "object" && f.workflow !== null && Array.isArray(f.workflow.steps);
|
|
5850
5907
|
}
|
|
5851
|
-
|
|
5852
5908
|
// src/rules/configSafety.ts
|
|
5853
5909
|
var MAX_STEP_CONFIG_BYTES = 256 * 1024;
|
|
5854
5910
|
var FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
@@ -6204,6 +6260,7 @@ export {
|
|
|
6204
6260
|
isQueryableConnector,
|
|
6205
6261
|
mappingTransformFunctionName,
|
|
6206
6262
|
migrateDelayConfig,
|
|
6263
|
+
missingContractRequired,
|
|
6207
6264
|
queryDefinition,
|
|
6208
6265
|
queryFieldsSatisfied,
|
|
6209
6266
|
queryTablesSatisfied,
|