@d34dman/flowdrop 0.0.25 → 0.0.27

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 (40) hide show
  1. package/README.md +52 -62
  2. package/dist/components/App.svelte +12 -2
  3. package/dist/components/ConfigForm.svelte +500 -9
  4. package/dist/components/ConfigForm.svelte.d.ts +2 -0
  5. package/dist/components/ConfigModal.svelte +4 -70
  6. package/dist/components/ConfigPanel.svelte +4 -9
  7. package/dist/components/EdgeRefresher.svelte +41 -0
  8. package/dist/components/EdgeRefresher.svelte.d.ts +9 -0
  9. package/dist/components/ReadOnlyDetails.svelte +3 -1
  10. package/dist/components/UniversalNode.svelte +6 -3
  11. package/dist/components/WorkflowEditor.svelte +30 -0
  12. package/dist/components/WorkflowEditor.svelte.d.ts +3 -1
  13. package/dist/components/form/FormCheckboxGroup.svelte +2 -9
  14. package/dist/components/form/FormField.svelte +1 -12
  15. package/dist/components/form/FormFieldWrapper.svelte +2 -10
  16. package/dist/components/form/FormFieldWrapper.svelte.d.ts +1 -1
  17. package/dist/components/form/FormMarkdownEditor.svelte +0 -2
  18. package/dist/components/form/FormNumberField.svelte +5 -6
  19. package/dist/components/form/FormRangeField.svelte +3 -13
  20. package/dist/components/form/FormSelect.svelte +4 -5
  21. package/dist/components/form/FormSelect.svelte.d.ts +1 -1
  22. package/dist/components/form/FormTextField.svelte +3 -4
  23. package/dist/components/form/FormTextarea.svelte +3 -4
  24. package/dist/components/form/FormToggle.svelte +2 -3
  25. package/dist/components/form/index.d.ts +14 -14
  26. package/dist/components/form/index.js +14 -14
  27. package/dist/components/form/types.d.ts +2 -2
  28. package/dist/components/form/types.js +1 -1
  29. package/dist/components/nodes/NotesNode.svelte +39 -45
  30. package/dist/components/nodes/NotesNode.svelte.d.ts +1 -1
  31. package/dist/components/nodes/SimpleNode.svelte +92 -142
  32. package/dist/components/nodes/SquareNode.svelte +75 -58
  33. package/dist/components/nodes/WorkflowNode.svelte +1 -3
  34. package/dist/index.d.ts +3 -1
  35. package/dist/index.js +2 -0
  36. package/dist/services/dynamicSchemaService.d.ts +108 -0
  37. package/dist/services/dynamicSchemaService.js +445 -0
  38. package/dist/styles/base.css +1 -1
  39. package/dist/types/index.d.ts +213 -0
  40. package/package.json +163 -155
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Dynamic Schema Service
3
+ * Handles fetching configuration schemas from REST endpoints at runtime.
4
+ * Used for nodes where the config schema cannot be determined at workflow load time.
5
+ *
6
+ * @module services/dynamicSchemaService
7
+ */
8
+ import type { ConfigSchema, DynamicSchemaEndpoint, ExternalEditLink, ConfigEditOptions, WorkflowNode } from '../types/index.js';
9
+ /**
10
+ * Result of a dynamic schema fetch operation
11
+ */
12
+ export interface DynamicSchemaResult {
13
+ /** Whether the fetch was successful */
14
+ success: boolean;
15
+ /** The fetched config schema (if successful) */
16
+ schema?: ConfigSchema;
17
+ /** Error message (if failed) */
18
+ error?: string;
19
+ /** Whether the schema was loaded from cache */
20
+ fromCache?: boolean;
21
+ }
22
+ /**
23
+ * Fetches a dynamic configuration schema from a REST endpoint.
24
+ *
25
+ * @param endpoint - The dynamic schema endpoint configuration
26
+ * @param node - The workflow node instance
27
+ * @param workflowId - Optional workflow ID for context
28
+ * @returns A promise that resolves to the schema result
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * const endpoint: DynamicSchemaEndpoint = {
33
+ * url: "/api/nodes/{nodeTypeId}/schema",
34
+ * method: "GET",
35
+ * parameterMapping: { nodeTypeId: "metadata.id" }
36
+ * };
37
+ *
38
+ * const result = await fetchDynamicSchema(endpoint, node);
39
+ * if (result.success && result.schema) {
40
+ * // Use the fetched schema
41
+ * }
42
+ * ```
43
+ */
44
+ export declare function fetchDynamicSchema(endpoint: DynamicSchemaEndpoint, node: WorkflowNode, workflowId?: string): Promise<DynamicSchemaResult>;
45
+ /**
46
+ * Resolves an external edit link URL with template variables.
47
+ *
48
+ * @param link - The external edit link configuration
49
+ * @param node - The workflow node instance
50
+ * @param workflowId - Optional workflow ID for context
51
+ * @param callbackUrl - Optional callback URL to append
52
+ * @returns The resolved URL string
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * const link: ExternalEditLink = {
57
+ * url: "https://admin.example.com/nodes/{nodeTypeId}/edit/{instanceId}",
58
+ * parameterMapping: { nodeTypeId: "metadata.id", instanceId: "id" }
59
+ * };
60
+ *
61
+ * const url = resolveExternalEditUrl(link, node, workflowId);
62
+ * // Returns "https://admin.example.com/nodes/llm-node/edit/node-1"
63
+ * ```
64
+ */
65
+ export declare function resolveExternalEditUrl(link: ExternalEditLink, node: WorkflowNode, workflowId?: string, callbackUrl?: string): string;
66
+ /**
67
+ * Gets the effective config edit options for a node.
68
+ * Merges node type defaults with instance-level overrides.
69
+ *
70
+ * @param node - The workflow node instance
71
+ * @returns The merged config edit options, or undefined if not configured
72
+ */
73
+ export declare function getEffectiveConfigEditOptions(node: WorkflowNode): ConfigEditOptions | undefined;
74
+ /**
75
+ * Clears the schema cache.
76
+ * Can optionally clear only entries matching a specific pattern.
77
+ *
78
+ * @param pattern - Optional pattern to match cache keys (e.g., node type ID)
79
+ */
80
+ export declare function clearSchemaCache(pattern?: string): void;
81
+ /**
82
+ * Invalidates a specific schema cache entry for a node.
83
+ *
84
+ * @param node - The workflow node to invalidate cache for
85
+ * @param endpoint - The dynamic schema endpoint configuration
86
+ */
87
+ export declare function invalidateSchemaCache(node: WorkflowNode, endpoint: DynamicSchemaEndpoint): void;
88
+ /**
89
+ * Checks if a node has config edit options configured.
90
+ *
91
+ * @param node - The workflow node to check
92
+ * @returns True if the node has config edit options configured
93
+ */
94
+ export declare function hasConfigEditOptions(node: WorkflowNode): boolean;
95
+ /**
96
+ * Determines if external edit should be shown for a node.
97
+ *
98
+ * @param node - The workflow node to check
99
+ * @returns True if external edit link should be shown
100
+ */
101
+ export declare function shouldShowExternalEdit(node: WorkflowNode): boolean;
102
+ /**
103
+ * Determines if dynamic schema should be used for a node.
104
+ *
105
+ * @param node - The workflow node to check
106
+ * @returns True if dynamic schema should be fetched
107
+ */
108
+ export declare function shouldUseDynamicSchema(node: WorkflowNode): boolean;
@@ -0,0 +1,445 @@
1
+ /**
2
+ * Dynamic Schema Service
3
+ * Handles fetching configuration schemas from REST endpoints at runtime.
4
+ * Used for nodes where the config schema cannot be determined at workflow load time.
5
+ *
6
+ * @module services/dynamicSchemaService
7
+ */
8
+ import { getEndpointConfig } from './api.js';
9
+ /**
10
+ * Schema cache with TTL support
11
+ * Key format: `{nodeTypeId}:{instanceId}` or `{nodeTypeId}` for type-level caching
12
+ */
13
+ const schemaCache = new Map();
14
+ /**
15
+ * Default cache TTL in milliseconds (5 minutes)
16
+ */
17
+ const DEFAULT_CACHE_TTL = 5 * 60 * 1000;
18
+ /**
19
+ * Resolves a template variable path from the node context.
20
+ * Supports dot-notation paths like "metadata.id", "config.apiKey", "id"
21
+ *
22
+ * @param context - The node context containing all available data
23
+ * @param path - Dot-notation path to resolve (e.g., "metadata.id")
24
+ * @returns The resolved value as a string, or undefined if not found
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * const context = { id: "node-1", metadata: { id: "llm-node" } };
29
+ * resolveVariablePath(context, "metadata.id"); // Returns "llm-node"
30
+ * resolveVariablePath(context, "id"); // Returns "node-1"
31
+ * ```
32
+ */
33
+ function resolveVariablePath(context, path) {
34
+ const parts = path.split('.');
35
+ let current = context;
36
+ for (const part of parts) {
37
+ if (current === null || current === undefined) {
38
+ return undefined;
39
+ }
40
+ if (typeof current === 'object' && part in current) {
41
+ current = current[part];
42
+ }
43
+ else {
44
+ return undefined;
45
+ }
46
+ }
47
+ // Convert to string if not already
48
+ if (current === null || current === undefined) {
49
+ return undefined;
50
+ }
51
+ return String(current);
52
+ }
53
+ /**
54
+ * Replaces template variables in a URL or string with values from the node context.
55
+ * Template variables use curly brace syntax: {variableName}
56
+ *
57
+ * @param template - The template string with variables
58
+ * @param parameterMapping - Maps variable names to context paths
59
+ * @param context - The node context containing all available data
60
+ * @returns The resolved string with all variables replaced
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * const url = "/api/nodes/{nodeTypeId}/schema?instance={instanceId}";
65
+ * const mapping = { nodeTypeId: "metadata.id", instanceId: "id" };
66
+ * const context = { id: "node-1", metadata: { id: "llm-node" } };
67
+ * resolveTemplate(url, mapping, context);
68
+ * // Returns "/api/nodes/llm-node/schema?instance=node-1"
69
+ * ```
70
+ */
71
+ function resolveTemplate(template, parameterMapping, context) {
72
+ if (!parameterMapping) {
73
+ return template;
74
+ }
75
+ let resolved = template;
76
+ // Replace each mapped variable
77
+ for (const [variableName, contextPath] of Object.entries(parameterMapping)) {
78
+ const value = resolveVariablePath(context, contextPath);
79
+ if (value !== undefined) {
80
+ // Use global regex to replace all occurrences
81
+ const regex = new RegExp(`\\{${variableName}\\}`, 'g');
82
+ resolved = resolved.replace(regex, encodeURIComponent(value));
83
+ }
84
+ }
85
+ // Also try to resolve any unmapped variables directly from context
86
+ const remainingVariables = resolved.match(/\{([^}]+)\}/g);
87
+ if (remainingVariables) {
88
+ for (const variable of remainingVariables) {
89
+ const variableName = variable.slice(1, -1); // Remove { and }
90
+ const value = resolveVariablePath(context, variableName);
91
+ if (value !== undefined) {
92
+ resolved = resolved.replace(variable, encodeURIComponent(value));
93
+ }
94
+ }
95
+ }
96
+ return resolved;
97
+ }
98
+ /**
99
+ * Generates a cache key for a schema based on the node context and endpoint configuration.
100
+ *
101
+ * @param endpoint - The dynamic schema endpoint configuration
102
+ * @param context - The node context
103
+ * @returns A unique cache key string
104
+ */
105
+ function generateCacheKey(endpoint, context) {
106
+ const url = resolveTemplate(endpoint.url, endpoint.parameterMapping, context);
107
+ return `schema:${url}`;
108
+ }
109
+ /**
110
+ * Checks if a cached schema is still valid (not expired).
111
+ *
112
+ * @param entry - The cache entry to check
113
+ * @param ttl - Time-to-live in milliseconds
114
+ * @returns True if the cache entry is still valid
115
+ */
116
+ function isCacheValid(entry, ttl = DEFAULT_CACHE_TTL) {
117
+ return Date.now() - entry.cachedAt < ttl;
118
+ }
119
+ /**
120
+ * Fetches a dynamic configuration schema from a REST endpoint.
121
+ *
122
+ * @param endpoint - The dynamic schema endpoint configuration
123
+ * @param node - The workflow node instance
124
+ * @param workflowId - Optional workflow ID for context
125
+ * @returns A promise that resolves to the schema result
126
+ *
127
+ * @example
128
+ * ```typescript
129
+ * const endpoint: DynamicSchemaEndpoint = {
130
+ * url: "/api/nodes/{nodeTypeId}/schema",
131
+ * method: "GET",
132
+ * parameterMapping: { nodeTypeId: "metadata.id" }
133
+ * };
134
+ *
135
+ * const result = await fetchDynamicSchema(endpoint, node);
136
+ * if (result.success && result.schema) {
137
+ * // Use the fetched schema
138
+ * }
139
+ * ```
140
+ */
141
+ export async function fetchDynamicSchema(endpoint, node, workflowId) {
142
+ // Build the context from the node
143
+ const context = {
144
+ id: node.id,
145
+ type: node.type,
146
+ metadata: node.data.metadata,
147
+ config: node.data.config,
148
+ extensions: node.data.extensions,
149
+ workflowId
150
+ };
151
+ // Generate cache key
152
+ const cacheKey = generateCacheKey(endpoint, context);
153
+ // Check cache first (if caching is enabled)
154
+ if (endpoint.cacheSchema !== false) {
155
+ const cached = schemaCache.get(cacheKey);
156
+ if (cached && isCacheValid(cached)) {
157
+ return {
158
+ success: true,
159
+ schema: cached.schema,
160
+ fromCache: true
161
+ };
162
+ }
163
+ }
164
+ // Resolve the URL with template variables
165
+ let url = resolveTemplate(endpoint.url, endpoint.parameterMapping, context);
166
+ // If URL is relative, try to prepend base URL from endpoint config
167
+ if (url.startsWith('/')) {
168
+ const currentConfig = getEndpointConfig();
169
+ if (currentConfig?.baseUrl) {
170
+ // Remove trailing slash from base URL and leading slash from relative URL
171
+ const baseUrl = currentConfig.baseUrl.replace(/\/$/, '');
172
+ url = `${baseUrl}${url}`;
173
+ }
174
+ }
175
+ // Prepare request options
176
+ const method = endpoint.method ?? 'GET';
177
+ const timeout = endpoint.timeout ?? 10000;
178
+ const headers = {
179
+ Accept: 'application/json',
180
+ 'Content-Type': 'application/json',
181
+ ...endpoint.headers
182
+ };
183
+ // Add auth headers from endpoint config if available
184
+ const currentConfig = getEndpointConfig();
185
+ if (currentConfig?.auth) {
186
+ if (currentConfig.auth.type === 'bearer' && currentConfig.auth.token) {
187
+ headers['Authorization'] = `Bearer ${currentConfig.auth.token}`;
188
+ }
189
+ else if (currentConfig.auth.type === 'api_key' && currentConfig.auth.apiKey) {
190
+ headers['X-API-Key'] = currentConfig.auth.apiKey;
191
+ }
192
+ else if (currentConfig.auth.type === 'custom' && currentConfig.auth.headers) {
193
+ Object.assign(headers, currentConfig.auth.headers);
194
+ }
195
+ }
196
+ // Prepare fetch options
197
+ const fetchOptions = {
198
+ method,
199
+ headers,
200
+ signal: AbortSignal.timeout(timeout)
201
+ };
202
+ // Add body for non-GET requests
203
+ if (method !== 'GET' && endpoint.body) {
204
+ // Resolve any template variables in the body
205
+ const resolvedBody = {};
206
+ for (const [key, value] of Object.entries(endpoint.body)) {
207
+ if (typeof value === 'string') {
208
+ resolvedBody[key] = resolveTemplate(value, endpoint.parameterMapping, context);
209
+ }
210
+ else {
211
+ resolvedBody[key] = value;
212
+ }
213
+ }
214
+ fetchOptions.body = JSON.stringify(resolvedBody);
215
+ }
216
+ try {
217
+ const response = await fetch(url, fetchOptions);
218
+ if (!response.ok) {
219
+ const errorText = await response.text();
220
+ return {
221
+ success: false,
222
+ error: `HTTP ${response.status}: ${errorText || response.statusText}`
223
+ };
224
+ }
225
+ const data = await response.json();
226
+ // The response could be:
227
+ // 1. Direct ConfigSchema object
228
+ // 2. Wrapped in { data: ConfigSchema } or { schema: ConfigSchema }
229
+ // 3. Wrapped in { success: true, data: ConfigSchema }
230
+ let schema;
231
+ if (data.type === 'object' && data.properties) {
232
+ // Direct ConfigSchema
233
+ schema = data;
234
+ }
235
+ else if (data.data?.type === 'object' && data.data?.properties) {
236
+ // Wrapped in { data: ... }
237
+ schema = data.data;
238
+ }
239
+ else if (data.schema?.type === 'object' && data.schema?.properties) {
240
+ // Wrapped in { schema: ... }
241
+ schema = data.schema;
242
+ }
243
+ else if (data.success && data.data?.type === 'object') {
244
+ // Wrapped in { success: true, data: ... }
245
+ schema = data.data;
246
+ }
247
+ if (!schema) {
248
+ return {
249
+ success: false,
250
+ error: 'Invalid schema format received from endpoint'
251
+ };
252
+ }
253
+ // Cache the schema (if caching is enabled)
254
+ if (endpoint.cacheSchema !== false) {
255
+ schemaCache.set(cacheKey, {
256
+ schema,
257
+ cachedAt: Date.now(),
258
+ cacheKey
259
+ });
260
+ }
261
+ return {
262
+ success: true,
263
+ schema,
264
+ fromCache: false
265
+ };
266
+ }
267
+ catch (error) {
268
+ // Handle specific error types
269
+ if (error instanceof Error) {
270
+ if (error.name === 'AbortError' || error.name === 'TimeoutError') {
271
+ return {
272
+ success: false,
273
+ error: `Request timed out after ${timeout}ms`
274
+ };
275
+ }
276
+ return {
277
+ success: false,
278
+ error: error.message
279
+ };
280
+ }
281
+ return {
282
+ success: false,
283
+ error: 'Unknown error occurred while fetching schema'
284
+ };
285
+ }
286
+ }
287
+ /**
288
+ * Resolves an external edit link URL with template variables.
289
+ *
290
+ * @param link - The external edit link configuration
291
+ * @param node - The workflow node instance
292
+ * @param workflowId - Optional workflow ID for context
293
+ * @param callbackUrl - Optional callback URL to append
294
+ * @returns The resolved URL string
295
+ *
296
+ * @example
297
+ * ```typescript
298
+ * const link: ExternalEditLink = {
299
+ * url: "https://admin.example.com/nodes/{nodeTypeId}/edit/{instanceId}",
300
+ * parameterMapping: { nodeTypeId: "metadata.id", instanceId: "id" }
301
+ * };
302
+ *
303
+ * const url = resolveExternalEditUrl(link, node, workflowId);
304
+ * // Returns "https://admin.example.com/nodes/llm-node/edit/node-1"
305
+ * ```
306
+ */
307
+ export function resolveExternalEditUrl(link, node, workflowId, callbackUrl) {
308
+ // Build the context from the node
309
+ const context = {
310
+ id: node.id,
311
+ type: node.type,
312
+ metadata: node.data.metadata,
313
+ config: node.data.config,
314
+ extensions: node.data.extensions,
315
+ workflowId
316
+ };
317
+ // Resolve the URL with template variables
318
+ let url = resolveTemplate(link.url, link.parameterMapping, context);
319
+ // Append callback URL if configured
320
+ if (callbackUrl && link.callbackUrlParam) {
321
+ const separator = url.includes('?') ? '&' : '?';
322
+ url = `${url}${separator}${link.callbackUrlParam}=${encodeURIComponent(callbackUrl)}`;
323
+ }
324
+ return url;
325
+ }
326
+ /**
327
+ * Gets the effective config edit options for a node.
328
+ * Merges node type defaults with instance-level overrides.
329
+ *
330
+ * @param node - The workflow node instance
331
+ * @returns The merged config edit options, or undefined if not configured
332
+ */
333
+ export function getEffectiveConfigEditOptions(node) {
334
+ const typeConfig = node.data.metadata?.configEdit;
335
+ const instanceConfig = node.data.extensions?.configEdit;
336
+ // If neither is defined, return undefined
337
+ if (!typeConfig && !instanceConfig) {
338
+ return undefined;
339
+ }
340
+ // If only one is defined, return it
341
+ if (!typeConfig) {
342
+ return instanceConfig;
343
+ }
344
+ if (!instanceConfig) {
345
+ return typeConfig;
346
+ }
347
+ // Merge both configurations (instance overrides type)
348
+ return {
349
+ ...typeConfig,
350
+ ...instanceConfig,
351
+ // Deep merge external edit link
352
+ externalEditLink: (instanceConfig.externalEditLink ?? typeConfig.externalEditLink)
353
+ ? {
354
+ ...typeConfig.externalEditLink,
355
+ ...instanceConfig.externalEditLink
356
+ }
357
+ : undefined,
358
+ // Deep merge dynamic schema
359
+ dynamicSchema: (instanceConfig.dynamicSchema ?? typeConfig.dynamicSchema)
360
+ ? {
361
+ ...typeConfig.dynamicSchema,
362
+ ...instanceConfig.dynamicSchema
363
+ }
364
+ : undefined
365
+ };
366
+ }
367
+ /**
368
+ * Clears the schema cache.
369
+ * Can optionally clear only entries matching a specific pattern.
370
+ *
371
+ * @param pattern - Optional pattern to match cache keys (e.g., node type ID)
372
+ */
373
+ export function clearSchemaCache(pattern) {
374
+ if (!pattern) {
375
+ schemaCache.clear();
376
+ return;
377
+ }
378
+ // Clear matching entries
379
+ for (const key of schemaCache.keys()) {
380
+ if (key.includes(pattern)) {
381
+ schemaCache.delete(key);
382
+ }
383
+ }
384
+ }
385
+ /**
386
+ * Invalidates a specific schema cache entry for a node.
387
+ *
388
+ * @param node - The workflow node to invalidate cache for
389
+ * @param endpoint - The dynamic schema endpoint configuration
390
+ */
391
+ export function invalidateSchemaCache(node, endpoint) {
392
+ const context = {
393
+ id: node.id,
394
+ type: node.type,
395
+ metadata: node.data.metadata,
396
+ config: node.data.config,
397
+ extensions: node.data.extensions
398
+ };
399
+ const cacheKey = generateCacheKey(endpoint, context);
400
+ schemaCache.delete(cacheKey);
401
+ }
402
+ /**
403
+ * Checks if a node has config edit options configured.
404
+ *
405
+ * @param node - The workflow node to check
406
+ * @returns True if the node has config edit options configured
407
+ */
408
+ export function hasConfigEditOptions(node) {
409
+ return getEffectiveConfigEditOptions(node) !== undefined;
410
+ }
411
+ /**
412
+ * Determines if external edit should be shown for a node.
413
+ *
414
+ * @param node - The workflow node to check
415
+ * @returns True if external edit link should be shown
416
+ */
417
+ export function shouldShowExternalEdit(node) {
418
+ const config = getEffectiveConfigEditOptions(node);
419
+ if (!config)
420
+ return false;
421
+ // Show external edit if configured and not preferring dynamic schema
422
+ if (config.externalEditLink) {
423
+ if (config.dynamicSchema && config.preferDynamicSchema) {
424
+ return false; // Prefer dynamic schema, so don't show external by default
425
+ }
426
+ return true;
427
+ }
428
+ return false;
429
+ }
430
+ /**
431
+ * Determines if dynamic schema should be used for a node.
432
+ *
433
+ * @param node - The workflow node to check
434
+ * @returns True if dynamic schema should be fetched
435
+ */
436
+ export function shouldUseDynamicSchema(node) {
437
+ const config = getEffectiveConfigEditOptions(node);
438
+ if (!config)
439
+ return false;
440
+ // Use dynamic schema if configured
441
+ if (config.dynamicSchema) {
442
+ return true;
443
+ }
444
+ return false;
445
+ }
@@ -1396,4 +1396,4 @@
1396
1396
 
1397
1397
  .markdown-display--large h3 {
1398
1398
  font-size: 1.25rem;
1399
- }
1399
+ }