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