animastor-comfyui-workflow-connector 0.1.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/LICENSE +21 -0
- package/README.md +298 -0
- package/package.json +41 -0
- package/src/connector-loader.js +978 -0
- package/src/entity-schema.js +340 -0
- package/src/index.js +265 -0
- package/src/workflow-loader.js +138 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
// ======================================================
|
|
2
|
+
// Entity Schema — Animastor Data Types
|
|
3
|
+
// ======================================================
|
|
4
|
+
// Defines the catalog of all data entities that can flow
|
|
5
|
+
// between backend and ComfyUI workflows.
|
|
6
|
+
//
|
|
7
|
+
// Each entity has:
|
|
8
|
+
// key — canonical identifier (used in connectors)
|
|
9
|
+
// label — human-readable name (for future UI)
|
|
10
|
+
// type — data type (string, image, audio, video, int, float, number)
|
|
11
|
+
// kind — 'input' | 'output' | 'parameter' | 'output'
|
|
12
|
+
//
|
|
13
|
+
// This schema serves as the foundation for:
|
|
14
|
+
// - Connector-to-workflow mapping validation
|
|
15
|
+
// - Future UI workflow configurator
|
|
16
|
+
// - Parameter editing interface
|
|
17
|
+
|
|
18
|
+
const ENTITIES = {
|
|
19
|
+
// ─── Text / Prompt ──────────────────────────────
|
|
20
|
+
positivePrompt: {
|
|
21
|
+
key: 'positivePrompt',
|
|
22
|
+
label: 'Positive Prompt',
|
|
23
|
+
type: 'string',
|
|
24
|
+
kind: 'input',
|
|
25
|
+
description: 'Main text prompt for generation (image or video)'
|
|
26
|
+
},
|
|
27
|
+
negativePrompt: {
|
|
28
|
+
key: 'negativePrompt',
|
|
29
|
+
label: 'Negative Prompt',
|
|
30
|
+
type: 'string',
|
|
31
|
+
kind: 'input',
|
|
32
|
+
description: 'Negative / undesired content prompt'
|
|
33
|
+
},
|
|
34
|
+
narrationText: {
|
|
35
|
+
key: 'narrationText',
|
|
36
|
+
label: 'Narration Text',
|
|
37
|
+
type: 'string',
|
|
38
|
+
kind: 'input',
|
|
39
|
+
description: 'Text to be synthesized as narration speech'
|
|
40
|
+
},
|
|
41
|
+
voiceInstruction: {
|
|
42
|
+
key: 'voiceInstruction',
|
|
43
|
+
label: 'Voice Instruction',
|
|
44
|
+
type: 'string',
|
|
45
|
+
kind: 'input',
|
|
46
|
+
description: 'Natural language description of the desired voice'
|
|
47
|
+
},
|
|
48
|
+
dialogueScript: {
|
|
49
|
+
key: 'dialogueScript',
|
|
50
|
+
label: 'Dialogue Script',
|
|
51
|
+
type: 'string',
|
|
52
|
+
kind: 'input',
|
|
53
|
+
description: 'Multi-character dialogue script with role markers'
|
|
54
|
+
},
|
|
55
|
+
defaultInstruct: {
|
|
56
|
+
key: 'defaultInstruct',
|
|
57
|
+
label: 'Default Instruction',
|
|
58
|
+
type: 'string',
|
|
59
|
+
kind: 'input',
|
|
60
|
+
description: 'Default TTS instruction for dialogue'
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// ─── Character Voices ───────────────────────────
|
|
64
|
+
character1Voice: {
|
|
65
|
+
key: 'character1Voice',
|
|
66
|
+
label: 'Character 1 Voice',
|
|
67
|
+
type: 'string',
|
|
68
|
+
kind: 'input',
|
|
69
|
+
description: 'Voice instruction for dialogue character 1'
|
|
70
|
+
},
|
|
71
|
+
character2Voice: {
|
|
72
|
+
key: 'character2Voice',
|
|
73
|
+
label: 'Character 2 Voice',
|
|
74
|
+
type: 'string',
|
|
75
|
+
kind: 'input',
|
|
76
|
+
description: 'Voice instruction for dialogue character 2'
|
|
77
|
+
},
|
|
78
|
+
roleName1: {
|
|
79
|
+
key: 'roleName1',
|
|
80
|
+
label: 'Role Name 1',
|
|
81
|
+
type: 'string',
|
|
82
|
+
kind: 'input',
|
|
83
|
+
description: 'Name/ID of dialogue character 1'
|
|
84
|
+
},
|
|
85
|
+
roleName2: {
|
|
86
|
+
key: 'roleName2',
|
|
87
|
+
label: 'Role Name 2',
|
|
88
|
+
type: 'string',
|
|
89
|
+
kind: 'input',
|
|
90
|
+
description: 'Name/ID of dialogue character 2'
|
|
91
|
+
},
|
|
92
|
+
character3Voice: {
|
|
93
|
+
key: 'character3Voice',
|
|
94
|
+
label: 'Character 3 Voice',
|
|
95
|
+
type: 'string',
|
|
96
|
+
kind: 'input',
|
|
97
|
+
description: 'Voice instruction for dialogue character 3'
|
|
98
|
+
},
|
|
99
|
+
roleName3: {
|
|
100
|
+
key: 'roleName3',
|
|
101
|
+
label: 'Role Name 3',
|
|
102
|
+
type: 'string',
|
|
103
|
+
kind: 'input',
|
|
104
|
+
description: 'Name/ID of dialogue character 3'
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
// ─── Images ─────────────────────────────────────
|
|
108
|
+
sourceImage: {
|
|
109
|
+
key: 'sourceImage',
|
|
110
|
+
label: 'Source Image',
|
|
111
|
+
type: 'image',
|
|
112
|
+
kind: 'input',
|
|
113
|
+
description: 'Single source image for conditioning'
|
|
114
|
+
},
|
|
115
|
+
sourceImages: {
|
|
116
|
+
key: 'sourceImages',
|
|
117
|
+
label: 'Source Images',
|
|
118
|
+
type: 'image[]',
|
|
119
|
+
kind: 'input',
|
|
120
|
+
description: 'Array of source images for multi-image workflows',
|
|
121
|
+
arrayLength: 4
|
|
122
|
+
},
|
|
123
|
+
mask: {
|
|
124
|
+
key: 'mask',
|
|
125
|
+
label: 'Mask',
|
|
126
|
+
type: 'image',
|
|
127
|
+
kind: 'input',
|
|
128
|
+
description: 'Binary mask for inpainting / compositing'
|
|
129
|
+
},
|
|
130
|
+
characterImage: {
|
|
131
|
+
key: 'characterImage',
|
|
132
|
+
label: 'Character Image',
|
|
133
|
+
type: 'image',
|
|
134
|
+
kind: 'input',
|
|
135
|
+
description: 'Character reference image for consistent appearance'
|
|
136
|
+
},
|
|
137
|
+
coverImage: {
|
|
138
|
+
key: 'coverImage',
|
|
139
|
+
label: 'Cover Image',
|
|
140
|
+
type: 'image',
|
|
141
|
+
kind: 'input',
|
|
142
|
+
description: 'Book cover image'
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
// ─── Audio ──────────────────────────────────────
|
|
146
|
+
audio: {
|
|
147
|
+
key: 'audio',
|
|
148
|
+
label: 'Audio',
|
|
149
|
+
type: 'audio',
|
|
150
|
+
kind: 'input',
|
|
151
|
+
description: 'Audio input for conditioning or merging'
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
// ─── Generated Outputs ──────────────────────────
|
|
155
|
+
generatedImage: {
|
|
156
|
+
key: 'generatedImage',
|
|
157
|
+
label: 'Generated Image',
|
|
158
|
+
type: 'image',
|
|
159
|
+
kind: 'output',
|
|
160
|
+
description: 'Image generated by the workflow'
|
|
161
|
+
},
|
|
162
|
+
generatedVideo: {
|
|
163
|
+
key: 'generatedVideo',
|
|
164
|
+
label: 'Generated Video',
|
|
165
|
+
type: 'video',
|
|
166
|
+
kind: 'output',
|
|
167
|
+
description: 'Video generated by the workflow'
|
|
168
|
+
},
|
|
169
|
+
generatedAudio: {
|
|
170
|
+
key: 'generatedAudio',
|
|
171
|
+
label: 'Generated Audio',
|
|
172
|
+
type: 'audio',
|
|
173
|
+
kind: 'output',
|
|
174
|
+
description: 'Audio generated by the workflow'
|
|
175
|
+
},
|
|
176
|
+
videoFrames: {
|
|
177
|
+
key: 'videoFrames',
|
|
178
|
+
label: 'Video Frames',
|
|
179
|
+
type: 'video',
|
|
180
|
+
kind: 'output',
|
|
181
|
+
description: 'Raw video frames from LTX model'
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
// ─── Parameters ─────────────────────────────────
|
|
185
|
+
totalFrames: {
|
|
186
|
+
key: 'totalFrames',
|
|
187
|
+
label: 'Total Frames',
|
|
188
|
+
type: 'int',
|
|
189
|
+
kind: 'parameter',
|
|
190
|
+
description: 'Total number of video frames to generate'
|
|
191
|
+
},
|
|
192
|
+
frameRate: {
|
|
193
|
+
key: 'frameRate',
|
|
194
|
+
label: 'Frame Rate',
|
|
195
|
+
type: 'float',
|
|
196
|
+
kind: 'parameter',
|
|
197
|
+
description: 'Video frame rate (FPS)'
|
|
198
|
+
},
|
|
199
|
+
width: {
|
|
200
|
+
key: 'width',
|
|
201
|
+
label: 'Width',
|
|
202
|
+
type: 'int',
|
|
203
|
+
kind: 'parameter',
|
|
204
|
+
description: 'Output image/video width in pixels'
|
|
205
|
+
},
|
|
206
|
+
height: {
|
|
207
|
+
key: 'height',
|
|
208
|
+
label: 'Height',
|
|
209
|
+
type: 'int',
|
|
210
|
+
kind: 'parameter',
|
|
211
|
+
description: 'Output image/video height in pixels'
|
|
212
|
+
},
|
|
213
|
+
steps: {
|
|
214
|
+
key: 'steps',
|
|
215
|
+
label: 'Steps',
|
|
216
|
+
type: 'int',
|
|
217
|
+
kind: 'parameter',
|
|
218
|
+
description: 'Number of diffusion sampling steps'
|
|
219
|
+
},
|
|
220
|
+
cfg: {
|
|
221
|
+
key: 'cfg',
|
|
222
|
+
label: 'CFG Scale',
|
|
223
|
+
type: 'float',
|
|
224
|
+
kind: 'parameter',
|
|
225
|
+
description: 'Classifier-free guidance scale'
|
|
226
|
+
},
|
|
227
|
+
sampler: {
|
|
228
|
+
key: 'sampler',
|
|
229
|
+
label: 'Sampler',
|
|
230
|
+
type: 'string',
|
|
231
|
+
kind: 'parameter',
|
|
232
|
+
description: 'Sampling method (euler, dpmpp_2m, etc.)'
|
|
233
|
+
},
|
|
234
|
+
scheduler: {
|
|
235
|
+
key: 'scheduler',
|
|
236
|
+
label: 'Scheduler',
|
|
237
|
+
type: 'string',
|
|
238
|
+
kind: 'parameter',
|
|
239
|
+
description: 'Noise scheduler (normal, karras, etc.)'
|
|
240
|
+
},
|
|
241
|
+
seed: {
|
|
242
|
+
key: 'seed',
|
|
243
|
+
label: 'Seed',
|
|
244
|
+
type: 'int',
|
|
245
|
+
kind: 'parameter',
|
|
246
|
+
description: 'Random seed for reproducibility'
|
|
247
|
+
},
|
|
248
|
+
outputFilenamePrefix: {
|
|
249
|
+
key: 'outputFilenamePrefix',
|
|
250
|
+
label: 'Output Filename Prefix',
|
|
251
|
+
type: 'string',
|
|
252
|
+
kind: 'parameter',
|
|
253
|
+
description: 'Prefix for output file names'
|
|
254
|
+
},
|
|
255
|
+
fps: {
|
|
256
|
+
key: 'fps',
|
|
257
|
+
label: 'FPS',
|
|
258
|
+
type: 'int',
|
|
259
|
+
kind: 'parameter',
|
|
260
|
+
description: 'Frames per second for video'
|
|
261
|
+
},
|
|
262
|
+
quality: {
|
|
263
|
+
key: 'quality',
|
|
264
|
+
label: 'Quality',
|
|
265
|
+
type: 'string',
|
|
266
|
+
kind: 'parameter',
|
|
267
|
+
description: 'Output quality setting (e.g. 320k for audio)'
|
|
268
|
+
},
|
|
269
|
+
language: {
|
|
270
|
+
key: 'language',
|
|
271
|
+
label: 'Language',
|
|
272
|
+
type: 'string',
|
|
273
|
+
kind: 'parameter',
|
|
274
|
+
description: 'Language for TTS output (e.g. Russian, English)'
|
|
275
|
+
},
|
|
276
|
+
temperature: {
|
|
277
|
+
key: 'temperature',
|
|
278
|
+
label: 'Temperature',
|
|
279
|
+
type: 'float',
|
|
280
|
+
kind: 'parameter',
|
|
281
|
+
description: 'Sampling temperature for generation randomness'
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
// ─── Guide Frame (LTX) ──────────────────────────
|
|
285
|
+
guideFrameIndex: {
|
|
286
|
+
key: 'guideFrameIndex',
|
|
287
|
+
label: 'Guide Frame Index',
|
|
288
|
+
type: 'int',
|
|
289
|
+
kind: 'parameter',
|
|
290
|
+
description: 'Frame index for LTXVAddGuide node'
|
|
291
|
+
},
|
|
292
|
+
guideStrength: {
|
|
293
|
+
key: 'guideStrength',
|
|
294
|
+
label: 'Guide Strength',
|
|
295
|
+
type: 'float',
|
|
296
|
+
kind: 'parameter',
|
|
297
|
+
description: 'Strength of the guide image influence'
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// ─── Quick Lookup ─────────────────────────────────
|
|
302
|
+
const BY_KEY = {};
|
|
303
|
+
|
|
304
|
+
for (const [key, def] of Object.entries(ENTITIES)) {
|
|
305
|
+
BY_KEY[key] = def;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Look up an entity definition by its key.
|
|
310
|
+
* @param {string} key
|
|
311
|
+
* @returns {object|undefined}
|
|
312
|
+
*/
|
|
313
|
+
function getEntity(key) {
|
|
314
|
+
return BY_KEY[key];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Return all entities of a given kind.
|
|
319
|
+
* @param {'input'|'output'|'parameter'} kind
|
|
320
|
+
* @returns {object[]}
|
|
321
|
+
*/
|
|
322
|
+
function getEntitiesByKind(kind) {
|
|
323
|
+
return Object.values(ENTITIES).filter(e => e.kind === kind);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Return all entities of a given data type.
|
|
328
|
+
* @param {string} type
|
|
329
|
+
* @returns {object[]}
|
|
330
|
+
*/
|
|
331
|
+
function getEntitiesByType(type) {
|
|
332
|
+
return Object.values(ENTITIES).filter(e => e.type === type);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
module.exports = {
|
|
336
|
+
ENTITIES,
|
|
337
|
+
getEntity,
|
|
338
|
+
getEntitiesByKind,
|
|
339
|
+
getEntitiesByType
|
|
340
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// ======================================================
|
|
2
|
+
// Connector API — public surface of the
|
|
3
|
+
// animastor-comfyui-workflow-connector package
|
|
4
|
+
// ======================================================
|
|
5
|
+
// A small dependency-injected adapter over the mapping core
|
|
6
|
+
// (workflow-loader + connector-loader + entity-schema). It presents the
|
|
7
|
+
// API the package exposes to its host:
|
|
8
|
+
//
|
|
9
|
+
// createWorkflowConnector({ workflowsDir, connectorsDir, logger })
|
|
10
|
+
// .listWorkflows() → [{ name, hash, type, label, hasConnector, compatible }]
|
|
11
|
+
// .getWorkflow(name) → deep-cloned workflow JSON
|
|
12
|
+
// .getConnector(name) → entity-level connector VIEW (no nodeIds/fields)
|
|
13
|
+
// .validate(name) → { compatible, warnings }
|
|
14
|
+
// .build({ workflow, inputs, parameters }) → { workflowJson, workflowHash }
|
|
15
|
+
//
|
|
16
|
+
// Extraction invariants:
|
|
17
|
+
// - ComfyUI node ids, field paths and the raw connector JSON format are
|
|
18
|
+
// NOT part of this API. Binding application happens inside build().
|
|
19
|
+
// - No dispatch, no queues, no GPU Hub, no DB/Redis, no business logic:
|
|
20
|
+
// this package may only depend on node builtins
|
|
21
|
+
// (guarded by tests/connector-core.test.js + the host architecture
|
|
22
|
+
// suite tests/architecture/comfyui-connector-core-boundary.test.js).
|
|
23
|
+
// - The host owns WHERE assets live (injected dirs) and HOW jobs are
|
|
24
|
+
// dispatched (gpu-dispatcher stays host-side).
|
|
25
|
+
|
|
26
|
+
const wfLoader = require('./workflow-loader');
|
|
27
|
+
const connectorLoader = require('./connector-loader');
|
|
28
|
+
|
|
29
|
+
// ─── Typed errors ───────────────────────────────────
|
|
30
|
+
|
|
31
|
+
class ConnectorApiError extends Error {
|
|
32
|
+
constructor(message, code) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = this.constructor.name;
|
|
35
|
+
this.code = code;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Requested workflow is not loaded. */
|
|
40
|
+
class WorkflowNotFoundError extends ConnectorApiError {
|
|
41
|
+
constructor(name) { super(`Workflow not found: ${name}`, 'WORKFLOW_NOT_FOUND'); }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Workflow has no connector registered (connectors are mandatory). */
|
|
45
|
+
class ConnectorMissingError extends ConnectorApiError {
|
|
46
|
+
constructor(name) {
|
|
47
|
+
super(`No connector registered for workflow "${name}". Connectors are mandatory for build().`,
|
|
48
|
+
'CONNECTOR_MISSING');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Workflow ↔ connector compatibility check failed (hash / node structure drift). */
|
|
53
|
+
class IncompatibleWorkflowError extends ConnectorApiError {
|
|
54
|
+
constructor(name, warnings) {
|
|
55
|
+
super(`Workflow "${name}" is incompatible with its connector:\n - ${(warnings || []).join('\n - ')}`,
|
|
56
|
+
'INCOMPATIBLE_WORKFLOW');
|
|
57
|
+
this.warnings = warnings || [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** build() received inputs/parameters that the connector does not map. */
|
|
62
|
+
class BuildError extends ConnectorApiError {
|
|
63
|
+
constructor(message) { super(message, 'BUILD_FAILED'); }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ─── Connector view (entity-level, node-id-free) ────
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Strip ComfyUI-specific internals (nodeId, field, expectedClass) from a
|
|
70
|
+
* binding section, keeping only the entity-level metadata.
|
|
71
|
+
*/
|
|
72
|
+
function sanitizeBindings(section) {
|
|
73
|
+
if (!section || typeof section !== 'object') return {};
|
|
74
|
+
const view = {};
|
|
75
|
+
for (const [key, binding] of Object.entries(section)) {
|
|
76
|
+
if (!binding || typeof binding !== 'object') continue;
|
|
77
|
+
if (binding.type === 'multi' && Array.isArray(binding.bindings)) {
|
|
78
|
+
view[key] = {
|
|
79
|
+
type: 'multi',
|
|
80
|
+
entityType: binding.entityType || key,
|
|
81
|
+
label: binding.label || key,
|
|
82
|
+
required: !!binding.required,
|
|
83
|
+
count: binding.bindings.length,
|
|
84
|
+
};
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
view[key] = {
|
|
88
|
+
entityType: binding.entityType || key,
|
|
89
|
+
label: binding.label || key,
|
|
90
|
+
required: !!binding.required,
|
|
91
|
+
};
|
|
92
|
+
if (binding.default !== undefined) view[key].default = binding.default;
|
|
93
|
+
if (binding.min !== undefined) view[key].min = binding.min;
|
|
94
|
+
if (binding.max !== undefined) view[key].max = binding.max;
|
|
95
|
+
}
|
|
96
|
+
return view;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Entity-level connector view: everything a business consumer may know
|
|
101
|
+
* about a workflow's ports WITHOUT seeing ComfyUI node ids or field paths.
|
|
102
|
+
*/
|
|
103
|
+
function toConnectorView(connector) {
|
|
104
|
+
if (!connector) return null;
|
|
105
|
+
return {
|
|
106
|
+
name: connector.workflow,
|
|
107
|
+
type: connector.type || 'unknown',
|
|
108
|
+
label: connector.label || connector.workflow,
|
|
109
|
+
description: connector.description || '',
|
|
110
|
+
version: connector.connectorVersion || '1.0.0',
|
|
111
|
+
profile: connector.profile || {},
|
|
112
|
+
inputs: sanitizeBindings(connector.inputs),
|
|
113
|
+
outputs: sanitizeBindings(connector.outputs),
|
|
114
|
+
parameters: sanitizeBindings(connector.parameters),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─── Public API factory ─────────────────────────────
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Create the workflow-connector API instance.
|
|
122
|
+
*
|
|
123
|
+
* @param {{ workflowsDir?: string, connectorsDir?: string, logger?: object }} [options]
|
|
124
|
+
* workflowsDir/connectorsDir — injected asset directories (host-owned);
|
|
125
|
+
* logger — injected logger (defaults to console).
|
|
126
|
+
* @returns the API object (listWorkflows/getWorkflow/getConnector/validate/build/…)
|
|
127
|
+
*/
|
|
128
|
+
function createWorkflowConnector({ workflowsDir, connectorsDir, logger } = {}) {
|
|
129
|
+
wfLoader.configure({ workflowsDir, logger });
|
|
130
|
+
connectorLoader.configure({ connectorsDir, logger });
|
|
131
|
+
|
|
132
|
+
// Load templates + connectors (connectors are mandatory: throws when a
|
|
133
|
+
// workflow lacks one — the same fail-closed semantics the host startup
|
|
134
|
+
// enforces via backend.cjs).
|
|
135
|
+
function load() {
|
|
136
|
+
return wfLoader.loadWorkflows();
|
|
137
|
+
}
|
|
138
|
+
load();
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
load,
|
|
142
|
+
|
|
143
|
+
/** All loaded workflows with entity-level metadata (no node ids). */
|
|
144
|
+
listWorkflows() {
|
|
145
|
+
const wfMap = wfLoader.workflows || {};
|
|
146
|
+
return Object.keys(wfMap).map((name) => {
|
|
147
|
+
const connector = connectorLoader.getConnector(name);
|
|
148
|
+
let compatible = null;
|
|
149
|
+
if (connector) {
|
|
150
|
+
compatible = connectorLoader.checkCompatibility(connector, wfMap[name]).compatible;
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
name,
|
|
154
|
+
hash: wfLoader.getWorkflowHash(name),
|
|
155
|
+
hasConnector: !!connector,
|
|
156
|
+
type: connector ? connector.type || 'unknown' : null,
|
|
157
|
+
label: connector ? connector.label || name : name,
|
|
158
|
+
compatible,
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
/** Deep-cloned workflow JSON by name. Throws WorkflowNotFoundError. */
|
|
164
|
+
getWorkflow(name) {
|
|
165
|
+
try {
|
|
166
|
+
return wfLoader.getWorkflow(name);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
throw new WorkflowNotFoundError(name);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Entity-level connector VIEW for a workflow (no nodeIds/fields).
|
|
174
|
+
* Throws ConnectorMissingError when the workflow has no connector.
|
|
175
|
+
*/
|
|
176
|
+
getConnector(name) {
|
|
177
|
+
const connector = connectorLoader.getConnector(name);
|
|
178
|
+
if (!connector) throw new ConnectorMissingError(name);
|
|
179
|
+
return toConnectorView(connector);
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Workflow ↔ connector compatibility check.
|
|
184
|
+
* Throws WorkflowNotFoundError; returns { compatible, warnings }.
|
|
185
|
+
*/
|
|
186
|
+
validate(name) {
|
|
187
|
+
const wfMap = wfLoader.workflows || {};
|
|
188
|
+
const wf = wfMap[name];
|
|
189
|
+
if (!wf) throw new WorkflowNotFoundError(name);
|
|
190
|
+
const connector = connectorLoader.getConnector(name);
|
|
191
|
+
if (!connector) throw new ConnectorMissingError(name);
|
|
192
|
+
return connectorLoader.checkCompatibility(connector, wf);
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Build a runnable workflow JSON from entity-keyed inputs and
|
|
197
|
+
* parameters. Node ids and field paths stay inside the connector.
|
|
198
|
+
*
|
|
199
|
+
* @param {{ workflow: string, inputs?: object, parameters?: object }} request
|
|
200
|
+
* inputs — { entityKey: value } applied via connector bindings
|
|
201
|
+
* parameters — { entityKey: value }; connector defaults fill gaps
|
|
202
|
+
* @returns {{ workflowJson: object, workflowHash: string }}
|
|
203
|
+
*/
|
|
204
|
+
build({ workflow, inputs = {}, parameters = {} } = {}) {
|
|
205
|
+
if (!workflow || typeof workflow !== 'string') {
|
|
206
|
+
throw new BuildError('build() requires a workflow name');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let wfJson;
|
|
210
|
+
try {
|
|
211
|
+
wfJson = wfLoader.getWorkflow(workflow);
|
|
212
|
+
} catch (err) {
|
|
213
|
+
throw new WorkflowNotFoundError(workflow);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const connector = connectorLoader.getConnector(workflow);
|
|
217
|
+
if (!connector) throw new ConnectorMissingError(workflow);
|
|
218
|
+
|
|
219
|
+
const compat = connectorLoader.checkCompatibility(connector, wfJson);
|
|
220
|
+
if (!compat.compatible) throw new IncompatibleWorkflowError(workflow, compat.warnings);
|
|
221
|
+
|
|
222
|
+
const unknownInputs = Object.keys(inputs).filter((k) => !connectorLoader.getBinding(connector, k));
|
|
223
|
+
if (unknownInputs.length > 0) {
|
|
224
|
+
throw new BuildError(`Unknown input key(s) for workflow "${workflow}": ${unknownInputs.join(', ')}`);
|
|
225
|
+
}
|
|
226
|
+
const unknownParams = Object.keys(parameters).filter((k) => !connector.parameters?.[k]);
|
|
227
|
+
if (unknownParams.length > 0) {
|
|
228
|
+
throw new BuildError(`Unknown parameter key(s) for workflow "${workflow}": ${unknownParams.join(', ')}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
for (const [key, value] of Object.entries(inputs)) {
|
|
232
|
+
connectorLoader.setValue(wfJson, connector, key, value);
|
|
233
|
+
}
|
|
234
|
+
for (const [key, param] of Object.entries(connector.parameters || {})) {
|
|
235
|
+
const value = parameters[key] !== undefined ? parameters[key] : param.default;
|
|
236
|
+
if (value === undefined) continue;
|
|
237
|
+
connectorLoader.setValue(wfJson, connector, key, value);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return { workflowJson: wfJson, workflowHash: wfLoader.getWorkflowHash(workflow) };
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
/** Computed sha256 of a loaded workflow template. */
|
|
244
|
+
getWorkflowHash(name) {
|
|
245
|
+
return wfLoader.getWorkflowHash(name);
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
module.exports = {
|
|
251
|
+
createWorkflowConnector,
|
|
252
|
+
ConnectorApiError,
|
|
253
|
+
WorkflowNotFoundError,
|
|
254
|
+
ConnectorMissingError,
|
|
255
|
+
IncompatibleWorkflowError,
|
|
256
|
+
BuildError,
|
|
257
|
+
|
|
258
|
+
// Compatibility surface (extraction §6.1): the loader singletons remain
|
|
259
|
+
// reachable so host consumers migrated from backend/src/workflows/*
|
|
260
|
+
// keep their exact call shapes. The createWorkflowConnector() factory
|
|
261
|
+
// is the preferred long-term API.
|
|
262
|
+
workflowLoader: wfLoader,
|
|
263
|
+
connectorLoader,
|
|
264
|
+
entitySchema: require('./entity-schema'),
|
|
265
|
+
};
|