@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.
- package/README.md +52 -62
- package/dist/components/App.svelte +12 -2
- package/dist/components/ConfigForm.svelte +500 -9
- package/dist/components/ConfigForm.svelte.d.ts +2 -0
- package/dist/components/ConfigModal.svelte +4 -70
- package/dist/components/ConfigPanel.svelte +4 -9
- package/dist/components/EdgeRefresher.svelte +41 -0
- package/dist/components/EdgeRefresher.svelte.d.ts +9 -0
- package/dist/components/ReadOnlyDetails.svelte +3 -1
- package/dist/components/UniversalNode.svelte +6 -3
- package/dist/components/WorkflowEditor.svelte +30 -0
- package/dist/components/WorkflowEditor.svelte.d.ts +3 -1
- package/dist/components/form/FormCheckboxGroup.svelte +2 -9
- package/dist/components/form/FormField.svelte +1 -12
- package/dist/components/form/FormFieldWrapper.svelte +2 -10
- package/dist/components/form/FormFieldWrapper.svelte.d.ts +1 -1
- package/dist/components/form/FormMarkdownEditor.svelte +0 -2
- package/dist/components/form/FormNumberField.svelte +5 -6
- package/dist/components/form/FormRangeField.svelte +3 -13
- package/dist/components/form/FormSelect.svelte +4 -5
- package/dist/components/form/FormSelect.svelte.d.ts +1 -1
- package/dist/components/form/FormTextField.svelte +3 -4
- package/dist/components/form/FormTextarea.svelte +3 -4
- package/dist/components/form/FormToggle.svelte +2 -3
- package/dist/components/form/index.d.ts +14 -14
- package/dist/components/form/index.js +14 -14
- package/dist/components/form/types.d.ts +2 -2
- package/dist/components/form/types.js +1 -1
- package/dist/components/nodes/NotesNode.svelte +39 -45
- package/dist/components/nodes/NotesNode.svelte.d.ts +1 -1
- package/dist/components/nodes/SimpleNode.svelte +92 -142
- package/dist/components/nodes/SquareNode.svelte +75 -58
- package/dist/components/nodes/WorkflowNode.svelte +1 -3
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/services/dynamicSchemaService.d.ts +108 -0
- package/dist/services/dynamicSchemaService.js +445 -0
- package/dist/styles/base.css +1 -1
- package/dist/types/index.d.ts +213 -0
- 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
|
+
}
|
package/dist/styles/base.css
CHANGED