@octaviaflow/flow-rules 0.19.1 → 0.20.0

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