@d34dman/flowdrop 0.0.47 → 0.0.49

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.
Files changed (36) hide show
  1. package/dist/components/App.svelte +11 -0
  2. package/dist/components/App.svelte.d.ts +2 -0
  3. package/dist/components/ConfigForm.svelte +31 -5
  4. package/dist/components/ConfigForm.svelte.d.ts +3 -1
  5. package/dist/components/NodeSidebar.svelte +1 -0
  6. package/dist/components/SchemaForm.svelte +4 -2
  7. package/dist/components/SettingsPanel.svelte +3 -3
  8. package/dist/components/form/FormAutocomplete.svelte +9 -4
  9. package/dist/components/form/FormCodeEditor.svelte +17 -15
  10. package/dist/components/form/FormField.svelte +32 -4
  11. package/dist/components/form/FormField.svelte.d.ts +11 -0
  12. package/dist/components/form/FormFieldLight.svelte +0 -1
  13. package/dist/components/form/FormTemplateEditor.svelte +300 -79
  14. package/dist/components/form/FormTemplateEditor.svelte.d.ts +11 -7
  15. package/dist/components/form/index.d.ts +1 -1
  16. package/dist/components/form/index.js +1 -1
  17. package/dist/components/form/templateAutocomplete.d.ts +2 -11
  18. package/dist/components/form/templateAutocomplete.js +49 -104
  19. package/dist/components/form/types.d.ts +0 -6
  20. package/dist/components/nodes/TerminalNode.svelte +27 -15
  21. package/dist/components/nodes/ToolNode.svelte +4 -6
  22. package/dist/services/apiVariableService.d.ts +116 -0
  23. package/dist/services/apiVariableService.js +338 -0
  24. package/dist/services/globalSave.js +6 -0
  25. package/dist/services/variableService.d.ts +50 -9
  26. package/dist/services/variableService.js +139 -44
  27. package/dist/svelte-app.d.ts +5 -0
  28. package/dist/svelte-app.js +7 -1
  29. package/dist/types/index.d.ts +138 -1
  30. package/dist/types/settings.js +4 -4
  31. package/dist/utils/colors.js +1 -0
  32. package/dist/utils/handlePositioning.d.ts +31 -0
  33. package/dist/utils/handlePositioning.js +35 -0
  34. package/dist/utils/icons.js +1 -0
  35. package/dist/utils/nodeTypes.js +3 -3
  36. package/package.json +8 -4
@@ -0,0 +1,338 @@
1
+ /**
2
+ * API Variable Service
3
+ * Handles fetching template variable schemas from REST endpoints at runtime.
4
+ * Enables dynamic variable suggestions from backend APIs for template editors.
5
+ *
6
+ * @module services/apiVariableService
7
+ */
8
+ import { getEndpointConfig } from './api.js';
9
+ /**
10
+ * Variable schema cache with TTL support
11
+ * Key format: `variables:{workflowId}:{nodeId}`
12
+ */
13
+ const variableCache = new Map();
14
+ /**
15
+ * Default cache TTL in milliseconds (5 minutes)
16
+ */
17
+ export const DEFAULT_VARIABLE_CACHE_TTL = 5 * 60 * 1000;
18
+ /**
19
+ * Replaces {workflowId} and {nodeId} placeholders in URL template.
20
+ *
21
+ * @param template - The URL template string
22
+ * @param context - The variable context with workflowId and nodeId
23
+ * @returns The resolved URL with placeholders replaced
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * const url = "/api/variables/{workflowId}/{nodeId}";
28
+ * const context = { workflowId: "wf-123", nodeId: "node-456" };
29
+ * resolveEndpointUrl(url, context);
30
+ * // Returns "/api/variables/wf-123/node-456"
31
+ * ```
32
+ */
33
+ export function resolveEndpointUrl(template, context) {
34
+ let resolved = template;
35
+ // Replace {workflowId}
36
+ if (context.workflowId) {
37
+ resolved = resolved.replace(/\{workflowId\}/g, encodeURIComponent(context.workflowId));
38
+ }
39
+ // Replace {nodeId}
40
+ resolved = resolved.replace(/\{nodeId\}/g, encodeURIComponent(context.nodeId));
41
+ return resolved;
42
+ }
43
+ /**
44
+ * Generates a cache key for a variable schema based on the context.
45
+ *
46
+ * @param workflowId - The workflow ID (optional)
47
+ * @param nodeId - The node instance ID
48
+ * @returns A unique cache key string
49
+ */
50
+ function generateVariableCacheKey(workflowId, nodeId) {
51
+ return `variables:${workflowId || 'unknown'}:${nodeId}`;
52
+ }
53
+ /**
54
+ * Checks if a cached variable schema is still valid (not expired).
55
+ *
56
+ * @param entry - The cache entry to check
57
+ * @param ttl - Time-to-live in milliseconds
58
+ * @returns True if the cache entry is still valid
59
+ */
60
+ function isCacheValid(entry, ttl = DEFAULT_VARIABLE_CACHE_TTL) {
61
+ return Date.now() - entry.cachedAt < ttl;
62
+ }
63
+ /**
64
+ * Resolves template variables in request body.
65
+ * Recursively processes nested objects and arrays.
66
+ *
67
+ * @param body - The request body object
68
+ * @param context - The variable context
69
+ * @returns The body with template variables resolved
70
+ */
71
+ function resolveBodyTemplates(body, context) {
72
+ const resolved = {};
73
+ for (const [key, value] of Object.entries(body)) {
74
+ if (typeof value === 'string') {
75
+ // Resolve template variables in string values
76
+ let resolvedValue = value;
77
+ if (context.workflowId) {
78
+ resolvedValue = resolvedValue.replace(/\{workflowId\}/g, encodeURIComponent(context.workflowId));
79
+ }
80
+ resolvedValue = resolvedValue.replace(/\{nodeId\}/g, encodeURIComponent(context.nodeId));
81
+ resolved[key] = resolvedValue;
82
+ }
83
+ else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
84
+ // Recursively resolve nested objects
85
+ resolved[key] = resolveBodyTemplates(value, context);
86
+ }
87
+ else {
88
+ // Pass through other values as-is
89
+ resolved[key] = value;
90
+ }
91
+ }
92
+ return resolved;
93
+ }
94
+ /**
95
+ * Fetches variable schema from a backend API endpoint.
96
+ *
97
+ * @param workflowId - The workflow ID (optional)
98
+ * @param nodeId - The node instance ID
99
+ * @param config - The API variables configuration
100
+ * @param authProvider - Optional auth provider for auth headers
101
+ * @returns A promise that resolves to the variable schema result
102
+ *
103
+ * @example
104
+ * ```typescript
105
+ * const config: ApiVariablesConfig = {
106
+ * endpoint: {
107
+ * url: "/api/variables/{workflowId}/{nodeId}",
108
+ * method: "GET"
109
+ * },
110
+ * cacheTtl: 300000
111
+ * };
112
+ *
113
+ * const result = await fetchVariableSchema("wf-123", "node-456", config);
114
+ * if (result.success && result.schema) {
115
+ * // Use the fetched variable schema
116
+ * }
117
+ * ```
118
+ */
119
+ export async function fetchVariableSchema(workflowId, nodeId, config, authProvider) {
120
+ const endpoint = config.endpoint;
121
+ const context = { workflowId, nodeId };
122
+ // Generate cache key
123
+ const cacheKey = generateVariableCacheKey(workflowId, nodeId);
124
+ // Check cache first (if caching is enabled)
125
+ if (endpoint.cacheEnabled !== false) {
126
+ const cached = variableCache.get(cacheKey);
127
+ const ttl = config.cacheTtl ?? DEFAULT_VARIABLE_CACHE_TTL;
128
+ if (cached && isCacheValid(cached, ttl)) {
129
+ return {
130
+ success: true,
131
+ schema: cached.schema,
132
+ fromCache: true
133
+ };
134
+ }
135
+ }
136
+ // Resolve the URL with template variables
137
+ let url = resolveEndpointUrl(endpoint.url, context);
138
+ // If URL is relative, prepend base URL from endpoint config
139
+ if (url.startsWith('/')) {
140
+ const currentConfig = getEndpointConfig();
141
+ if (currentConfig?.baseUrl) {
142
+ const baseUrl = currentConfig.baseUrl.replace(/\/$/, '');
143
+ url = `${baseUrl}${url}`;
144
+ }
145
+ }
146
+ // Prepare request options
147
+ const method = endpoint.method ?? 'GET';
148
+ const timeout = endpoint.timeout ?? 30000;
149
+ const headers = {
150
+ Accept: 'application/json',
151
+ 'Content-Type': 'application/json',
152
+ ...endpoint.headers
153
+ };
154
+ // Add auth headers from AuthProvider if available
155
+ if (authProvider) {
156
+ try {
157
+ const authHeaders = await authProvider.getAuthHeaders();
158
+ Object.assign(headers, authHeaders);
159
+ }
160
+ catch (error) {
161
+ console.warn('Failed to get auth headers:', error);
162
+ }
163
+ }
164
+ // Add auth headers from endpoint config as fallback
165
+ const currentConfig = getEndpointConfig();
166
+ if (currentConfig?.auth) {
167
+ if (currentConfig.auth.type === 'bearer' && currentConfig.auth.token) {
168
+ headers['Authorization'] = headers['Authorization'] ?? `Bearer ${currentConfig.auth.token}`;
169
+ }
170
+ else if (currentConfig.auth.type === 'api_key' && currentConfig.auth.apiKey) {
171
+ headers['X-API-Key'] = headers['X-API-Key'] ?? currentConfig.auth.apiKey;
172
+ }
173
+ else if (currentConfig.auth.type === 'custom' && currentConfig.auth.headers) {
174
+ Object.assign(headers, currentConfig.auth.headers);
175
+ }
176
+ }
177
+ // Prepare fetch options
178
+ const fetchOptions = {
179
+ method,
180
+ headers,
181
+ signal: AbortSignal.timeout(timeout)
182
+ };
183
+ // Add body for non-GET requests
184
+ if (method !== 'GET' && endpoint.body) {
185
+ const resolvedBody = resolveBodyTemplates(endpoint.body, context);
186
+ fetchOptions.body = JSON.stringify(resolvedBody);
187
+ }
188
+ try {
189
+ const response = await fetch(url, fetchOptions);
190
+ // Handle 404 as "no variables available"
191
+ if (response.status === 404) {
192
+ return {
193
+ success: true,
194
+ schema: { variables: {} },
195
+ fromCache: false
196
+ };
197
+ }
198
+ if (!response.ok) {
199
+ // Handle authentication errors
200
+ if (response.status === 401 || response.status === 403) {
201
+ if (authProvider?.onUnauthorized) {
202
+ const refreshed = await authProvider.onUnauthorized();
203
+ if (refreshed) {
204
+ // Retry with refreshed auth
205
+ return fetchVariableSchema(workflowId, nodeId, config, authProvider);
206
+ }
207
+ }
208
+ return {
209
+ success: false,
210
+ error: 'Authentication failed'
211
+ };
212
+ }
213
+ const errorText = await response.text();
214
+ return {
215
+ success: false,
216
+ error: `HTTP ${response.status}: ${errorText || response.statusText}`
217
+ };
218
+ }
219
+ const data = await response.json();
220
+ // The response could be:
221
+ // 1. Direct VariableSchema: { variables: {...} }
222
+ // 2. Wrapped in { data: { variables: {...} } }
223
+ // 3. Wrapped in { schema: { variables: {...} } }
224
+ // 4. Wrapped in { success: true, data: { variables: {...} } }
225
+ let schema;
226
+ if (data.variables && typeof data.variables === 'object') {
227
+ // Direct VariableSchema
228
+ schema = data;
229
+ }
230
+ else if (data.data?.variables && typeof data.data.variables === 'object') {
231
+ // Wrapped in { data: ... }
232
+ schema = data.data;
233
+ }
234
+ else if (data.schema?.variables && typeof data.schema.variables === 'object') {
235
+ // Wrapped in { schema: ... }
236
+ schema = data.schema;
237
+ }
238
+ else if (data.success && data.data?.variables) {
239
+ // Wrapped in { success: true, data: ... }
240
+ schema = data.data;
241
+ }
242
+ if (!schema) {
243
+ return {
244
+ success: false,
245
+ error: 'Invalid variable schema format received from endpoint'
246
+ };
247
+ }
248
+ // Cache the schema (if caching is enabled)
249
+ if (endpoint.cacheEnabled !== false) {
250
+ variableCache.set(cacheKey, {
251
+ schema,
252
+ cachedAt: Date.now(),
253
+ cacheKey
254
+ });
255
+ }
256
+ return {
257
+ success: true,
258
+ schema,
259
+ fromCache: false
260
+ };
261
+ }
262
+ catch (error) {
263
+ // Handle specific error types
264
+ if (error instanceof Error) {
265
+ if (error.name === 'AbortError' || error.name === 'TimeoutError') {
266
+ return {
267
+ success: false,
268
+ error: `Request timed out after ${timeout}ms`
269
+ };
270
+ }
271
+ return {
272
+ success: false,
273
+ error: error.message
274
+ };
275
+ }
276
+ return {
277
+ success: false,
278
+ error: 'Unknown error occurred while fetching variable schema'
279
+ };
280
+ }
281
+ }
282
+ /**
283
+ * Clears the variable schema cache.
284
+ * Can optionally clear only entries matching a specific pattern.
285
+ *
286
+ * @param pattern - Optional pattern to match cache keys (e.g., workflow ID or node ID)
287
+ *
288
+ * @example
289
+ * ```typescript
290
+ * // Clear all cache
291
+ * clearVariableCache();
292
+ *
293
+ * // Clear cache for a specific workflow
294
+ * clearVariableCache("wf-123");
295
+ *
296
+ * // Clear cache for a specific node
297
+ * clearVariableCache("node-456");
298
+ * ```
299
+ */
300
+ export function clearVariableCache(pattern) {
301
+ if (!pattern) {
302
+ variableCache.clear();
303
+ return;
304
+ }
305
+ // Clear matching entries
306
+ for (const key of variableCache.keys()) {
307
+ if (key.includes(pattern)) {
308
+ variableCache.delete(key);
309
+ }
310
+ }
311
+ }
312
+ /**
313
+ * Invalidates a specific variable cache entry.
314
+ *
315
+ * @param workflowId - The workflow ID (optional)
316
+ * @param nodeId - The node instance ID
317
+ */
318
+ export function invalidateVariableCache(workflowId, nodeId) {
319
+ const cacheKey = generateVariableCacheKey(workflowId, nodeId);
320
+ variableCache.delete(cacheKey);
321
+ }
322
+ /**
323
+ * Gets the current cache size (number of entries).
324
+ *
325
+ * @returns The number of cached variable schemas
326
+ */
327
+ export function getVariableCacheSize() {
328
+ return variableCache.size;
329
+ }
330
+ /**
331
+ * Gets all cache keys currently stored.
332
+ * Useful for debugging and monitoring.
333
+ *
334
+ * @returns Array of cache keys
335
+ */
336
+ export function getVariableCacheKeys() {
337
+ return Array.from(variableCache.keys());
338
+ }
@@ -62,6 +62,12 @@ async function ensureApiConfiguration() {
62
62
  * Uses the current workflow from the global store
63
63
  */
64
64
  export async function globalSaveWorkflow() {
65
+ // Flush any pending form changes by blurring the active element.
66
+ // This ensures focusout handlers (like ConfigForm's handleFormBlur)
67
+ // sync local state to the global store before we read it.
68
+ if (typeof document !== 'undefined' && document.activeElement instanceof HTMLElement) {
69
+ document.activeElement.blur();
70
+ }
65
71
  let loadingToast;
66
72
  try {
67
73
  // Show loading toast
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * @module services/variableService
7
7
  */
8
- import type { WorkflowNode, WorkflowEdge, VariableSchema, TemplateVariable } from "../types/index.js";
8
+ import type { WorkflowNode, WorkflowEdge, VariableSchema, TemplateVariable, TemplateVariablesConfig, AuthProvider } from '../types/index.js';
9
9
  /**
10
10
  * Options for deriving available variables.
11
11
  */
@@ -46,14 +46,6 @@ export interface GetAvailableVariablesOptions {
46
46
  * ```
47
47
  */
48
48
  export declare function getAvailableVariables(node: WorkflowNode, nodes: WorkflowNode[], edges: WorkflowEdge[], options?: GetAvailableVariablesOptions): VariableSchema;
49
- /**
50
- * Converts simple variable hints (string array) to a VariableSchema.
51
- * Used for backward compatibility with the old variableHints format.
52
- *
53
- * @param hints - Array of variable names
54
- * @returns A VariableSchema with basic string variables
55
- */
56
- export declare function hintsToVariableSchema(hints: string[]): VariableSchema;
57
49
  /**
58
50
  * Gets the child variables for a given path in the variable schema.
59
51
  * Used for drilling down into nested objects and arrays.
@@ -98,3 +90,52 @@ export declare function isArrayVariable(schema: VariableSchema, path: string): b
98
90
  * @returns True if the variable has children that can be drilled into
99
91
  */
100
92
  export declare function hasChildren(schema: VariableSchema, path: string): boolean;
93
+ /**
94
+ * Merges two variable schemas together.
95
+ * Variables from the primary schema take precedence over the secondary schema.
96
+ *
97
+ * @param primary - The primary variable schema (takes precedence)
98
+ * @param secondary - The secondary variable schema
99
+ * @returns A merged VariableSchema
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * const apiSchema = { variables: { apiKey: {...}, endpoint: {...} } };
104
+ * const staticSchema = { variables: { env: {...}, config: {...} } };
105
+ * const merged = mergeVariableSchemas(apiSchema, staticSchema);
106
+ * // Result: { variables: { apiKey, endpoint, env, config } }
107
+ * ```
108
+ */
109
+ export declare function mergeVariableSchemas(primary: VariableSchema, secondary: VariableSchema): VariableSchema;
110
+ /**
111
+ * Gets variable schema using the appropriate mode (API, schema-based, or hybrid).
112
+ * This is the main orchestration function that determines how to fetch variables
113
+ * based on the configuration.
114
+ *
115
+ * @param node - The current node being configured
116
+ * @param nodes - All nodes in the workflow
117
+ * @param edges - All edges in the workflow
118
+ * @param config - Template variables configuration
119
+ * @param workflowId - Optional workflow ID for API context
120
+ * @param authProvider - Optional auth provider for API requests
121
+ * @returns A promise that resolves to the variable schema
122
+ *
123
+ * @example
124
+ * ```typescript
125
+ * // Schema-based mode (existing behavior)
126
+ * const config = { ports: ["data"], schema: {...} };
127
+ * const schema = await getVariableSchema(node, nodes, edges, config);
128
+ *
129
+ * // API mode
130
+ * const config = { api: { endpoint: { url: "/api/variables/{workflowId}/{nodeId}" } } };
131
+ * const schema = await getVariableSchema(node, nodes, edges, config, workflowId, authProvider);
132
+ *
133
+ * // Hybrid mode (API + static schema)
134
+ * const config = {
135
+ * schema: {...},
136
+ * api: { endpoint: {...}, mergeWithSchema: true }
137
+ * };
138
+ * const schema = await getVariableSchema(node, nodes, edges, config, workflowId, authProvider);
139
+ * ```
140
+ */
141
+ export declare function getVariableSchema(node: WorkflowNode, nodes: WorkflowNode[], edges: WorkflowEdge[], config: TemplateVariablesConfig, workflowId?: string, authProvider?: AuthProvider): Promise<VariableSchema>;
@@ -13,22 +13,22 @@
13
13
  */
14
14
  function toTemplateVariableType(schemaType) {
15
15
  switch (schemaType) {
16
- case "string":
17
- return "string";
18
- case "number":
19
- return "number";
20
- case "integer":
21
- return "integer";
22
- case "boolean":
23
- return "boolean";
24
- case "array":
25
- return "array";
26
- case "object":
27
- return "object";
28
- case "float":
29
- return "float";
16
+ case 'string':
17
+ return 'string';
18
+ case 'number':
19
+ return 'number';
20
+ case 'integer':
21
+ return 'integer';
22
+ case 'boolean':
23
+ return 'boolean';
24
+ case 'array':
25
+ return 'array';
26
+ case 'object':
27
+ return 'object';
28
+ case 'float':
29
+ return 'float';
30
30
  default:
31
- return "mixed";
31
+ return 'mixed';
32
32
  }
33
33
  }
34
34
  /**
@@ -51,15 +51,15 @@ function propertyToTemplateVariable(name, property, sourcePort, sourceNode) {
51
51
  sourceNode
52
52
  };
53
53
  // Handle nested object properties
54
- if (property.type === "object" && property.properties) {
54
+ if (property.type === 'object' && property.properties) {
55
55
  variable.properties = {};
56
56
  for (const [propName, propValue] of Object.entries(property.properties)) {
57
57
  variable.properties[propName] = propertyToTemplateVariable(propName, propValue, sourcePort, sourceNode);
58
58
  }
59
59
  }
60
60
  // Handle array items
61
- if (property.type === "array" && property.items) {
62
- variable.items = propertyToTemplateVariable("item", property.items, sourcePort, sourceNode);
61
+ if (property.type === 'array' && property.items) {
62
+ variable.items = propertyToTemplateVariable('item', property.items, sourcePort, sourceNode);
63
63
  }
64
64
  return variable;
65
65
  }
@@ -78,7 +78,7 @@ function portToTemplateVariable(port, sourceNode) {
78
78
  name: port.id,
79
79
  label: port.name,
80
80
  description: port.description,
81
- type: "object",
81
+ type: 'object',
82
82
  sourcePort: port.id,
83
83
  sourceNode,
84
84
  properties: {}
@@ -186,12 +186,12 @@ export function getAvailableVariables(node, nodes, edges, options) {
186
186
  for (const connection of connections) {
187
187
  const { sourceNode, sourcePort, targetPort } = connection;
188
188
  // Skip trigger ports - they don't carry data
189
- if (sourcePort?.dataType === "trigger")
189
+ if (sourcePort?.dataType === 'trigger')
190
190
  continue;
191
- if (targetPort?.dataType === "trigger")
191
+ if (targetPort?.dataType === 'trigger')
192
192
  continue;
193
193
  // Get the target port ID for filtering
194
- const targetPortId = targetPort?.id ?? sourcePort?.id ?? "data";
194
+ const targetPortId = targetPort?.id ?? sourcePort?.id ?? 'data';
195
195
  // Filter by target port IDs if specified
196
196
  if (targetPortIds !== undefined) {
197
197
  if (!targetPortIds.includes(targetPortId))
@@ -212,7 +212,7 @@ export function getAvailableVariables(node, nodes, edges, options) {
212
212
  }
213
213
  else {
214
214
  // No schema or includePortName is true - use port name as the variable
215
- const variableName = includePortName ? targetPortId : (targetPortId);
215
+ const variableName = includePortName ? targetPortId : targetPortId;
216
216
  // Skip if we already have a variable with this name
217
217
  if (variables[variableName])
218
218
  continue;
@@ -224,23 +224,6 @@ export function getAvailableVariables(node, nodes, edges, options) {
224
224
  }
225
225
  return { variables };
226
226
  }
227
- /**
228
- * Converts simple variable hints (string array) to a VariableSchema.
229
- * Used for backward compatibility with the old variableHints format.
230
- *
231
- * @param hints - Array of variable names
232
- * @returns A VariableSchema with basic string variables
233
- */
234
- export function hintsToVariableSchema(hints) {
235
- const variables = {};
236
- for (const hint of hints) {
237
- variables[hint] = {
238
- name: hint,
239
- type: "mixed"
240
- };
241
- }
242
- return { variables };
243
- }
244
227
  /**
245
228
  * Gets the child variables for a given path in the variable schema.
246
229
  * Used for drilling down into nested objects and arrays.
@@ -261,7 +244,7 @@ export function hintsToVariableSchema(hints) {
261
244
  * ```
262
245
  */
263
246
  export function getChildVariables(schema, path) {
264
- const parts = path.split(".");
247
+ const parts = path.split('.');
265
248
  let current;
266
249
  // Navigate to the target variable
267
250
  for (let i = 0; i < parts.length; i++) {
@@ -312,7 +295,7 @@ export function getArrayIndexSuggestions(maxIndex = 2) {
312
295
  suggestions.push(`${i}]`);
313
296
  }
314
297
  // Add wildcard for "all items"
315
- suggestions.push("*]");
298
+ suggestions.push('*]');
316
299
  return suggestions;
317
300
  }
318
301
  /**
@@ -323,7 +306,7 @@ export function getArrayIndexSuggestions(maxIndex = 2) {
323
306
  * @returns True if the variable is an array type
324
307
  */
325
308
  export function isArrayVariable(schema, path) {
326
- const parts = path.split(".");
309
+ const parts = path.split('.');
327
310
  let current;
328
311
  for (let i = 0; i < parts.length; i++) {
329
312
  const part = parts[i];
@@ -352,7 +335,7 @@ export function isArrayVariable(schema, path) {
352
335
  return false;
353
336
  }
354
337
  }
355
- return current?.type === "array";
338
+ return current?.type === 'array';
356
339
  }
357
340
  /**
358
341
  * Checks if a variable at the given path has child properties.
@@ -365,3 +348,115 @@ export function hasChildren(schema, path) {
365
348
  const children = getChildVariables(schema, path);
366
349
  return children.length > 0;
367
350
  }
351
+ /**
352
+ * Merges two variable schemas together.
353
+ * Variables from the primary schema take precedence over the secondary schema.
354
+ *
355
+ * @param primary - The primary variable schema (takes precedence)
356
+ * @param secondary - The secondary variable schema
357
+ * @returns A merged VariableSchema
358
+ *
359
+ * @example
360
+ * ```typescript
361
+ * const apiSchema = { variables: { apiKey: {...}, endpoint: {...} } };
362
+ * const staticSchema = { variables: { env: {...}, config: {...} } };
363
+ * const merged = mergeVariableSchemas(apiSchema, staticSchema);
364
+ * // Result: { variables: { apiKey, endpoint, env, config } }
365
+ * ```
366
+ */
367
+ export function mergeVariableSchemas(primary, secondary) {
368
+ // Create a shallow copy of secondary variables
369
+ const mergedVariables = { ...secondary.variables };
370
+ // Overlay primary variables (they take precedence)
371
+ for (const [key, value] of Object.entries(primary.variables)) {
372
+ mergedVariables[key] = value;
373
+ }
374
+ return { variables: mergedVariables };
375
+ }
376
+ /**
377
+ * Gets variable schema using the appropriate mode (API, schema-based, or hybrid).
378
+ * This is the main orchestration function that determines how to fetch variables
379
+ * based on the configuration.
380
+ *
381
+ * @param node - The current node being configured
382
+ * @param nodes - All nodes in the workflow
383
+ * @param edges - All edges in the workflow
384
+ * @param config - Template variables configuration
385
+ * @param workflowId - Optional workflow ID for API context
386
+ * @param authProvider - Optional auth provider for API requests
387
+ * @returns A promise that resolves to the variable schema
388
+ *
389
+ * @example
390
+ * ```typescript
391
+ * // Schema-based mode (existing behavior)
392
+ * const config = { ports: ["data"], schema: {...} };
393
+ * const schema = await getVariableSchema(node, nodes, edges, config);
394
+ *
395
+ * // API mode
396
+ * const config = { api: { endpoint: { url: "/api/variables/{workflowId}/{nodeId}" } } };
397
+ * const schema = await getVariableSchema(node, nodes, edges, config, workflowId, authProvider);
398
+ *
399
+ * // Hybrid mode (API + static schema)
400
+ * const config = {
401
+ * schema: {...},
402
+ * api: { endpoint: {...}, mergeWithSchema: true }
403
+ * };
404
+ * const schema = await getVariableSchema(node, nodes, edges, config, workflowId, authProvider);
405
+ * ```
406
+ */
407
+ export async function getVariableSchema(node, nodes, edges, config, workflowId, authProvider) {
408
+ let resultSchema = { variables: {} };
409
+ // Try API mode first (if configured)
410
+ if (config.api) {
411
+ try {
412
+ // Import API variable service dynamically to avoid circular dependencies
413
+ const { fetchVariableSchema } = await import('./apiVariableService.js');
414
+ const apiResult = await fetchVariableSchema(workflowId, node.id, config.api, authProvider);
415
+ if (apiResult.success && apiResult.schema) {
416
+ resultSchema = apiResult.schema;
417
+ // Merge with static schema if configured
418
+ if (config.api.mergeWithSchema !== false && config.schema) {
419
+ resultSchema = mergeVariableSchemas(resultSchema, config.schema);
420
+ }
421
+ // Merge with port-derived variables if configured
422
+ if (config.api.mergeWithPorts) {
423
+ const portSchema = getAvailableVariables(node, nodes, edges, {
424
+ targetPortIds: config.ports,
425
+ includePortName: config.includePortName
426
+ });
427
+ resultSchema = mergeVariableSchemas(resultSchema, portSchema);
428
+ }
429
+ return resultSchema;
430
+ }
431
+ else if (!config.api.fallbackOnError) {
432
+ // API failed and fallback is disabled - return empty schema
433
+ console.error('Failed to fetch variables from API:', apiResult.error);
434
+ return { variables: {} };
435
+ }
436
+ // If fallback is enabled (default), continue to schema-based mode below
437
+ }
438
+ catch (error) {
439
+ console.error('Error fetching variables from API:', error);
440
+ // If fallback is disabled, return empty schema
441
+ if (config.api.fallbackOnError === false) {
442
+ return { variables: {} };
443
+ }
444
+ // Otherwise, continue to schema-based mode below
445
+ }
446
+ }
447
+ // Schema-based mode (existing behavior)
448
+ // This is the fallback when API mode is not configured or fails
449
+ // Start with port-derived variables (if ports are configured or no API mode)
450
+ if (config.ports !== undefined || !config.api) {
451
+ const portSchema = getAvailableVariables(node, nodes, edges, {
452
+ targetPortIds: config.ports,
453
+ includePortName: config.includePortName
454
+ });
455
+ resultSchema = portSchema;
456
+ }
457
+ // Merge with static schema (if configured)
458
+ if (config.schema) {
459
+ resultSchema = mergeVariableSchemas(config.schema, resultSchema);
460
+ }
461
+ return resultSchema;
462
+ }