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