ugcinc 4.0.2 → 4.1.1

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 (161) hide show
  1. package/dist/accounts.d.ts +146 -1
  2. package/dist/automations/index.d.ts +174 -0
  3. package/dist/automations/index.js +194 -0
  4. package/dist/automations/nodes/account.d.ts +6 -0
  5. package/dist/automations/nodes/account.js +28 -0
  6. package/dist/automations/nodes/auto-caption.d.ts +23 -0
  7. package/dist/automations/nodes/auto-caption.js +49 -0
  8. package/dist/automations/nodes/auto-post.d.ts +22 -0
  9. package/dist/automations/nodes/auto-post.js +52 -0
  10. package/dist/automations/nodes/branch.d.ts +21 -0
  11. package/dist/automations/nodes/branch.js +46 -0
  12. package/dist/automations/nodes/collect.d.ts +7 -0
  13. package/dist/automations/nodes/collect.js +51 -0
  14. package/dist/automations/nodes/compose-workflow.d.ts +8 -0
  15. package/dist/automations/nodes/compose-workflow.js +61 -0
  16. package/dist/automations/nodes/create-dm.d.ts +35 -0
  17. package/dist/automations/nodes/create-dm.js +86 -0
  18. package/dist/automations/nodes/custom-model.d.ts +18 -0
  19. package/dist/automations/nodes/custom-model.js +52 -0
  20. package/dist/automations/nodes/deduplicate.d.ts +7 -0
  21. package/dist/automations/nodes/deduplicate.js +36 -0
  22. package/dist/automations/nodes/destructure.d.ts +25 -0
  23. package/dist/automations/nodes/destructure.js +65 -0
  24. package/dist/automations/nodes/for-each.d.ts +22 -0
  25. package/dist/automations/nodes/for-each.js +84 -0
  26. package/dist/automations/nodes/generate-image.d.ts +15 -0
  27. package/dist/automations/nodes/generate-image.js +45 -0
  28. package/dist/automations/nodes/generate-video.d.ts +15 -0
  29. package/dist/automations/nodes/generate-video.js +45 -0
  30. package/dist/automations/nodes/if.d.ts +21 -0
  31. package/dist/automations/nodes/if.js +44 -0
  32. package/dist/automations/nodes/image-composer.d.ts +46 -0
  33. package/dist/automations/nodes/image-composer.js +135 -0
  34. package/dist/automations/nodes/index.d.ts +312 -0
  35. package/dist/automations/nodes/index.js +92 -0
  36. package/dist/automations/nodes/internal.d.ts +8 -0
  37. package/dist/automations/nodes/internal.js +10 -0
  38. package/dist/automations/nodes/llm.d.ts +26 -0
  39. package/dist/automations/nodes/llm.js +85 -0
  40. package/dist/automations/nodes/manual-trigger.d.ts +14 -0
  41. package/dist/automations/nodes/manual-trigger.js +32 -0
  42. package/dist/automations/nodes/media.d.ts +13 -0
  43. package/dist/automations/nodes/media.js +40 -0
  44. package/dist/automations/nodes/not.d.ts +5 -0
  45. package/dist/automations/nodes/not.js +35 -0
  46. package/dist/automations/nodes/output.d.ts +16 -0
  47. package/dist/automations/nodes/output.js +32 -0
  48. package/dist/automations/nodes/random-route.d.ts +24 -0
  49. package/dist/automations/nodes/random-route.js +53 -0
  50. package/dist/automations/nodes/random.d.ts +16 -0
  51. package/dist/automations/nodes/random.js +50 -0
  52. package/dist/automations/nodes/recurrence.d.ts +29 -0
  53. package/dist/automations/nodes/recurrence.js +50 -0
  54. package/dist/automations/nodes/save-to-media.d.ts +15 -0
  55. package/dist/automations/nodes/save-to-media.js +31 -0
  56. package/dist/automations/nodes/screenshot-animation.d.ts +6 -0
  57. package/dist/automations/nodes/screenshot-animation.js +36 -0
  58. package/dist/automations/nodes/social-audio.d.ts +7 -0
  59. package/dist/automations/nodes/social-audio.js +30 -0
  60. package/dist/automations/nodes/text.d.ts +6 -0
  61. package/dist/automations/nodes/text.js +41 -0
  62. package/dist/automations/nodes/transcript.d.ts +5 -0
  63. package/dist/automations/nodes/transcript.js +46 -0
  64. package/dist/automations/nodes/types.d.ts +202 -0
  65. package/dist/automations/nodes/types.js +22 -0
  66. package/dist/automations/nodes/video-composer.d.ts +16 -0
  67. package/dist/automations/nodes/video-composer.js +117 -0
  68. package/dist/automations/nodes/video-import.d.ts +9 -0
  69. package/dist/automations/nodes/video-import.js +23 -0
  70. package/dist/automations/types.d.ts +431 -0
  71. package/dist/automations/types.js +29 -0
  72. package/dist/automations.d.ts +5 -33
  73. package/dist/automations.js +6 -647
  74. package/dist/comments.d.ts +26 -1
  75. package/dist/comments.js +3 -0
  76. package/dist/graph-controller.d.ts +211 -0
  77. package/dist/graph-controller.js +656 -0
  78. package/dist/index.d.ts +22 -9
  79. package/dist/index.js +47 -24
  80. package/dist/internal-utils.d.ts +8 -0
  81. package/dist/internal-utils.js +22 -0
  82. package/dist/media.d.ts +135 -1
  83. package/dist/nodes/account.d.ts +7 -0
  84. package/dist/nodes/account.js +29 -0
  85. package/dist/nodes/auto-caption.d.ts +17 -0
  86. package/dist/nodes/auto-caption.js +46 -0
  87. package/dist/nodes/auto-post.d.ts +21 -0
  88. package/dist/nodes/auto-post.js +54 -0
  89. package/dist/nodes/branch.d.ts +12 -0
  90. package/dist/nodes/branch.js +50 -0
  91. package/dist/nodes/collect.d.ts +6 -0
  92. package/dist/nodes/collect.js +56 -0
  93. package/dist/nodes/compose-workflow.d.ts +21 -0
  94. package/dist/nodes/compose-workflow.js +42 -0
  95. package/dist/nodes/create-dm.d.ts +40 -0
  96. package/dist/nodes/create-dm.js +88 -0
  97. package/dist/nodes/custom-model.d.ts +19 -0
  98. package/dist/nodes/custom-model.js +52 -0
  99. package/dist/nodes/deduplicate.d.ts +8 -0
  100. package/dist/nodes/deduplicate.js +36 -0
  101. package/dist/nodes/destructure.d.ts +25 -0
  102. package/dist/nodes/destructure.js +65 -0
  103. package/dist/nodes/for-each.d.ts +23 -0
  104. package/dist/nodes/for-each.js +84 -0
  105. package/dist/nodes/generate-image.d.ts +16 -0
  106. package/dist/nodes/generate-image.js +45 -0
  107. package/dist/nodes/generate-video.d.ts +16 -0
  108. package/dist/nodes/generate-video.js +45 -0
  109. package/dist/nodes/if.d.ts +22 -0
  110. package/dist/nodes/if.js +44 -0
  111. package/dist/nodes/image-composer.d.ts +14 -0
  112. package/dist/nodes/image-composer.js +95 -0
  113. package/dist/nodes/index.d.ts +20 -0
  114. package/dist/nodes/index.js +93 -0
  115. package/dist/nodes/llm.d.ts +27 -0
  116. package/dist/nodes/llm.js +85 -0
  117. package/dist/nodes/manual-trigger.d.ts +16 -0
  118. package/dist/nodes/manual-trigger.js +32 -0
  119. package/dist/nodes/media.d.ts +17 -0
  120. package/dist/nodes/media.js +40 -0
  121. package/dist/nodes/not.d.ts +6 -0
  122. package/dist/nodes/not.js +35 -0
  123. package/dist/nodes/output.d.ts +9 -0
  124. package/dist/nodes/output.js +32 -0
  125. package/dist/nodes/random-route.d.ts +3 -0
  126. package/dist/nodes/random-route.js +50 -0
  127. package/dist/nodes/random.d.ts +3 -0
  128. package/dist/nodes/random.js +48 -0
  129. package/dist/nodes/recurrence.d.ts +3 -0
  130. package/dist/nodes/recurrence.js +45 -0
  131. package/dist/nodes/save-to-media.d.ts +3 -0
  132. package/dist/nodes/save-to-media.js +26 -0
  133. package/dist/nodes/screenshot-animation.d.ts +7 -0
  134. package/dist/nodes/screenshot-animation.js +36 -0
  135. package/dist/nodes/social-audio.d.ts +3 -0
  136. package/dist/nodes/social-audio.js +26 -0
  137. package/dist/nodes/text.d.ts +3 -0
  138. package/dist/nodes/text.js +38 -0
  139. package/dist/nodes/transcript.d.ts +3 -0
  140. package/dist/nodes/transcript.js +42 -0
  141. package/dist/nodes/types.d.ts +146 -0
  142. package/dist/nodes/types.js +22 -0
  143. package/dist/nodes/video-composer.d.ts +3 -0
  144. package/dist/nodes/video-composer.js +67 -0
  145. package/dist/nodes/video-import.d.ts +3 -0
  146. package/dist/nodes/video-import.js +35 -0
  147. package/dist/org.d.ts +13 -1
  148. package/dist/ports.js +3 -9
  149. package/dist/posts.d.ts +88 -1
  150. package/dist/render/compositions/IMessageDmComposition/types.d.ts +24 -24
  151. package/dist/render/compositions/ImageEditorComposition.js +2 -8
  152. package/dist/render/compositions/VideoEditorComposition.js +2 -24
  153. package/dist/render/types/element.d.ts +0 -33
  154. package/dist/render/types/index.d.ts +1 -1
  155. package/dist/render/types/video.d.ts +2 -2
  156. package/dist/render.d.ts +2 -1
  157. package/dist/stats.d.ts +128 -1
  158. package/dist/tasks.d.ts +20 -1
  159. package/dist/types.d.ts +1 -2216
  160. package/dist/types.js +2 -124
  161. package/package.json +1 -1
@@ -0,0 +1,656 @@
1
+ "use strict";
2
+ /**
3
+ * Graph Controller
4
+ *
5
+ * Centralized logic for automation graph management including:
6
+ * - Port computation
7
+ * - Connection management
8
+ * - Type compatibility checking
9
+ * - Workflow validation
10
+ * - For-each context validation
11
+ *
12
+ * All functions work with WorkflowNodeDefinition[] (embedded connection format).
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.areTypesCompatible = areTypesCompatible;
16
+ exports.computePortsForNode = computePortsForNode;
17
+ exports.getAllNodes = getAllNodes;
18
+ exports.getNodeByType = getNodeByType;
19
+ exports.getOutputSchema = getOutputSchema;
20
+ exports.deriveConnections = deriveConnections;
21
+ exports.addConnection = addConnection;
22
+ exports.removeConnection = removeConnection;
23
+ exports.removeNodeConnections = removeNodeConnections;
24
+ exports.cleanupStaleConnections = cleanupStaleConnections;
25
+ exports.getForEachContext = getForEachContext;
26
+ exports.checkCrossContextViolation = checkCrossContextViolation;
27
+ exports.validateWorkflow = validateWorkflow;
28
+ exports.getErrorNodeIds = getErrorNodeIds;
29
+ exports.getPortErrorsForNode = getPortErrorsForNode;
30
+ exports.hasMissingTriggerError = hasMissingTriggerError;
31
+ exports.hasMissingTerminalError = hasMissingTerminalError;
32
+ exports.resolveNodePreview = resolveNodePreview;
33
+ const types_1 = require("./automations/types");
34
+ const nodes_1 = require("./automations/nodes");
35
+ // ===========================================================================
36
+ // Internal Helpers (not exported)
37
+ // ===========================================================================
38
+ /**
39
+ * Helper to check if a port type is a union (array of BasePortTypes)
40
+ */
41
+ function isTypeUnion(type) {
42
+ return Array.isArray(type);
43
+ }
44
+ /**
45
+ * Extract template variables from text (e.g., "Hello {{name}}" -> ["name"])
46
+ */
47
+ function extractTemplateVariables(texts) {
48
+ const variables = new Set();
49
+ const regex = /\{\{(\w+)\}\}/g;
50
+ for (const text of texts) {
51
+ let match;
52
+ while ((match = regex.exec(text)) !== null) {
53
+ if (match[1])
54
+ variables.add(match[1]);
55
+ }
56
+ }
57
+ return Array.from(variables);
58
+ }
59
+ /**
60
+ * Derive Connection[] array from nodes for internal processing
61
+ * Connections are stored in the embedded inputs map format.
62
+ */
63
+ function deriveConnectionsInternal(nodes) {
64
+ const connections = [];
65
+ nodes.forEach(node => {
66
+ if (!node.inputs)
67
+ return;
68
+ Object.entries(node.inputs).forEach(([inputId, mapping]) => {
69
+ connections.push({
70
+ id: `${mapping.sourceNodeId}-${mapping.sourceOutputId}-${node.id}-${inputId}`,
71
+ sourceNodeId: mapping.sourceNodeId,
72
+ sourceOutputId: mapping.sourceOutputId,
73
+ targetNodeId: node.id,
74
+ targetInputId: inputId,
75
+ });
76
+ });
77
+ });
78
+ return connections;
79
+ }
80
+ /**
81
+ * Get all nodes that are downstream of a for-each node (inside its context).
82
+ */
83
+ function getDownstreamNodes(forEachNodeId, nodes) {
84
+ const connections = deriveConnectionsInternal(nodes);
85
+ const downstream = new Set();
86
+ const queue = [];
87
+ // Find direct outputs from for-each
88
+ for (const conn of connections) {
89
+ if (conn.sourceNodeId === forEachNodeId) {
90
+ if (!downstream.has(conn.targetNodeId)) {
91
+ downstream.add(conn.targetNodeId);
92
+ queue.push(conn.targetNodeId);
93
+ }
94
+ }
95
+ }
96
+ // BFS to find all downstream
97
+ while (queue.length > 0) {
98
+ const currentId = queue.shift();
99
+ for (const conn of connections) {
100
+ if (conn.sourceNodeId === currentId && !downstream.has(conn.targetNodeId)) {
101
+ downstream.add(conn.targetNodeId);
102
+ queue.push(conn.targetNodeId);
103
+ }
104
+ }
105
+ }
106
+ return downstream;
107
+ }
108
+ /**
109
+ * Check if a node would be "captured" by a for-each context after a proposed connection.
110
+ *
111
+ * A node is captured if ALL of its outputs go to nodes inside the for-each context
112
+ * (either downstream nodes or other captured nodes).
113
+ */
114
+ function wouldBeCaptured(nodeId, forEachNodeId, downstreamNodes, nodes, proposedTargetNodeId) {
115
+ const node = nodes.find(n => n.id === nodeId);
116
+ if (!node)
117
+ return false;
118
+ // For-each nodes can't be captured
119
+ if (node.type === 'for-each')
120
+ return false;
121
+ // Already downstream = not captured (it's inside)
122
+ if (downstreamNodes.has(nodeId))
123
+ return false;
124
+ const connections = deriveConnectionsInternal(nodes);
125
+ // Get all targets this node outputs to (existing + proposed)
126
+ const outputTargets = new Set();
127
+ for (const conn of connections) {
128
+ if (conn.sourceNodeId === nodeId) {
129
+ outputTargets.add(conn.targetNodeId);
130
+ }
131
+ }
132
+ // Add the proposed connection target
133
+ outputTargets.add(proposedTargetNodeId);
134
+ // If no outputs, can't be captured
135
+ if (outputTargets.size === 0)
136
+ return false;
137
+ // Build captured set iteratively
138
+ const captured = new Set();
139
+ let changed = true;
140
+ while (changed) {
141
+ changed = false;
142
+ for (const n of nodes) {
143
+ if (n.id === forEachNodeId)
144
+ continue;
145
+ if (downstreamNodes.has(n.id))
146
+ continue;
147
+ if (captured.has(n.id))
148
+ continue;
149
+ // Get outputs for this node
150
+ const nodeOutputs = new Set();
151
+ for (const conn of connections) {
152
+ if (conn.sourceNodeId === n.id) {
153
+ nodeOutputs.add(conn.targetNodeId);
154
+ }
155
+ }
156
+ // Include proposed connection if this is the source node
157
+ if (n.id === nodeId) {
158
+ nodeOutputs.add(proposedTargetNodeId);
159
+ }
160
+ if (nodeOutputs.size === 0)
161
+ continue;
162
+ // All outputs must go to downstream or captured
163
+ const allInside = [...nodeOutputs].every(targetId => downstreamNodes.has(targetId) || captured.has(targetId));
164
+ if (allInside) {
165
+ captured.add(n.id);
166
+ changed = true;
167
+ }
168
+ }
169
+ }
170
+ return captured.has(nodeId);
171
+ }
172
+ /**
173
+ * Find the for-each context a node belongs to by tracing backwards.
174
+ *
175
+ * A node is "inside" a for-each context if, when tracing backwards through
176
+ * connections, we reach a for-each node via one of its OUTPUT ports.
177
+ *
178
+ * Returns the for-each node ID if inside a context, or null if outside all contexts.
179
+ */
180
+ function getForEachContextInternal(nodeId, nodes, connections, visited = new Set()) {
181
+ // Prevent infinite loops from circular references
182
+ if (visited.has(nodeId))
183
+ return null;
184
+ visited.add(nodeId);
185
+ const node = nodes.find(n => n.id === nodeId);
186
+ if (!node)
187
+ return null;
188
+ // Check all inputs to this node
189
+ for (const connection of connections) {
190
+ if (connection.targetNodeId !== nodeId)
191
+ continue;
192
+ const sourceNode = nodes.find(n => n.id === connection.sourceNodeId);
193
+ if (!sourceNode)
194
+ continue;
195
+ // If the source is a for-each node and we're connected to one of its outputs,
196
+ // then this node is inside that for-each context
197
+ if (sourceNode.type === 'for-each') {
198
+ return sourceNode.id;
199
+ }
200
+ // Otherwise, recursively check the source node's context
201
+ const sourceContext = getForEachContextInternal(connection.sourceNodeId, nodes, connections, visited);
202
+ if (sourceContext) {
203
+ return sourceContext;
204
+ }
205
+ }
206
+ return null;
207
+ }
208
+ // ===========================================================================
209
+ // Type Compatibility
210
+ // ===========================================================================
211
+ /**
212
+ * Check if two port types are compatible for connection
213
+ * A connection is valid if:
214
+ * 1. The types match exactly (same base type and same isArray), OR
215
+ * 2. The source is an array type and target is 'object' (non-array), OR
216
+ * 3. The source is a union and includes a compatible type, OR
217
+ * 4. The target is a union and includes a compatible type, OR
218
+ * 5. Both are unions and have at least one common compatible type
219
+ *
220
+ * Note: Array inputs are strict - they only accept exact matches
221
+ */
222
+ function areTypesCompatible({ sourceType, sourceIsArray, targetType, targetIsArray, }) {
223
+ const sourceTypes = isTypeUnion(sourceType) ? sourceType : [sourceType];
224
+ const targetTypes = isTypeUnion(targetType) ? targetType : [targetType];
225
+ return sourceTypes.some(st => {
226
+ return targetTypes.some(tt => {
227
+ // Exact match (same base type and same isArray)
228
+ if (st === tt && sourceIsArray === targetIsArray)
229
+ return true;
230
+ // Object inputs (non-array) accept any array type (backwards compatibility with for-each, etc.)
231
+ if (tt === 'object' && !targetIsArray && sourceIsArray) {
232
+ return true;
233
+ }
234
+ // Array inputs are strict - no other compatibility
235
+ if (targetIsArray) {
236
+ return false;
237
+ }
238
+ return false;
239
+ });
240
+ });
241
+ }
242
+ // ===========================================================================
243
+ // Port Computation
244
+ // ===========================================================================
245
+ /**
246
+ * Compute ports for a node based on its type and config.
247
+ * Uses the node definition's computePorts function.
248
+ *
249
+ * @param nodeType - The type of the node
250
+ * @param config - The node's configuration object
251
+ * @param client - Optional AutomationsClient for nodes that need to fetch data
252
+ * @param getConnectedOutput - Optional function to get connected output port info (for dynamic ports)
253
+ */
254
+ async function computePortsForNode({ nodeType, config, client, getConnectedOutput, }) {
255
+ const definition = (0, nodes_1.getNodeDefinition)(nodeType);
256
+ if (!definition) {
257
+ return { inputs: [], outputs: [] };
258
+ }
259
+ return await definition.computePorts({
260
+ config: config ?? definition.defaults,
261
+ client,
262
+ getConnectedOutput,
263
+ });
264
+ }
265
+ /**
266
+ * Get all available automation nodes.
267
+ * Converts node definitions from the registry into ComputedNode format.
268
+ */
269
+ async function getAllNodes(client) {
270
+ const definitions = (0, nodes_1.getAllNodeDefinitions)();
271
+ const results = [];
272
+ for (const def of definitions) {
273
+ const { inputs, outputs } = await def.computePorts({ config: def.defaults, client });
274
+ results.push({
275
+ nodeId: def.nodeId,
276
+ label: def.label,
277
+ description: def.description,
278
+ category: def.category,
279
+ type: def.type,
280
+ inputs,
281
+ outputs,
282
+ outputModes: def.outputModes,
283
+ selectionModes: def.selectionModes,
284
+ defaults: def.defaults,
285
+ });
286
+ }
287
+ return results;
288
+ }
289
+ /**
290
+ * Get a specific node by nodeId
291
+ */
292
+ async function getNodeByType(nodeId, client) {
293
+ const allNodes = await getAllNodes(client);
294
+ return allNodes.find(node => node.nodeId === nodeId);
295
+ }
296
+ // ===========================================================================
297
+ // Output Schema
298
+ // ===========================================================================
299
+ /**
300
+ * Get the schema of array items for a node's output port.
301
+ * Returns null if schema is unknown or the output is not an array.
302
+ *
303
+ * @param nodeType - The type of the node (e.g., 'transcript', 'llm')
304
+ * @param nodeConfig - The node's configuration object
305
+ * @param outputPortId - The ID of the output port to get schema for
306
+ * @param client - Optional AutomationsClient for nodes that need to fetch data
307
+ * @returns The schema of array items, or null if unknown
308
+ */
309
+ async function getOutputSchema({ nodeType, nodeConfig, outputPortId, client, }) {
310
+ // First check static node definitions
311
+ const nodeDefinition = await getNodeByType(nodeType, client);
312
+ const outputPort = nodeDefinition?.outputs.find(o => o.id === outputPortId);
313
+ if (outputPort?.objectSchema) {
314
+ return outputPort.objectSchema;
315
+ }
316
+ // Check resolvedPorts for nodes with dynamic outputs (compose-workflow, for-each, etc.)
317
+ if (nodeConfig?.resolvedPorts) {
318
+ const resolvedPorts = nodeConfig.resolvedPorts;
319
+ const resolvedOutput = resolvedPorts.outputs?.find(o => o.id === outputPortId);
320
+ if (resolvedOutput?.objectSchema) {
321
+ return resolvedOutput.objectSchema;
322
+ }
323
+ }
324
+ return null;
325
+ }
326
+ // ===========================================================================
327
+ // Connection Management
328
+ // ===========================================================================
329
+ /**
330
+ * Derive Connection[] array from nodes for canvas rendering
331
+ *
332
+ * Connections are stored in the embedded inputs map format.
333
+ * This function extracts them into a flat array for canvas rendering.
334
+ */
335
+ function deriveConnections({ nodes }) {
336
+ return deriveConnectionsInternal(nodes);
337
+ }
338
+ /**
339
+ * Add a connection to a node's inputs map
340
+ */
341
+ function addConnection({ nodes, sourceNodeId, sourceOutputId, targetNodeId, targetInputId, }) {
342
+ return nodes.map(node => {
343
+ if (node.id !== targetNodeId)
344
+ return node;
345
+ return {
346
+ ...node,
347
+ inputs: {
348
+ ...node.inputs,
349
+ [targetInputId]: {
350
+ sourceNodeId,
351
+ sourceOutputId,
352
+ },
353
+ },
354
+ };
355
+ });
356
+ }
357
+ /**
358
+ * Remove a connection from a node's inputs map
359
+ */
360
+ function removeConnection({ nodes, targetNodeId, targetInputId, }) {
361
+ return nodes.map(node => {
362
+ if (node.id !== targetNodeId)
363
+ return node;
364
+ const newInputs = { ...node.inputs };
365
+ delete newInputs[targetInputId];
366
+ return {
367
+ ...node,
368
+ inputs: newInputs,
369
+ };
370
+ });
371
+ }
372
+ /**
373
+ * Remove all connections to/from a node
374
+ */
375
+ function removeNodeConnections({ nodes, nodeId, }) {
376
+ return nodes.map(node => {
377
+ // Remove inputs that come from the deleted node
378
+ const newInputs = { ...node.inputs };
379
+ let hasChanges = false;
380
+ Object.entries(newInputs).forEach(([inputId, mapping]) => {
381
+ if (mapping.sourceNodeId === nodeId) {
382
+ delete newInputs[inputId];
383
+ hasChanges = true;
384
+ }
385
+ });
386
+ if (hasChanges) {
387
+ return { ...node, inputs: newInputs };
388
+ }
389
+ return node;
390
+ });
391
+ }
392
+ /**
393
+ * Clean up stale connections on load.
394
+ *
395
+ * Removes entries from node.inputs that reference ports that no longer exist
396
+ * in the node's resolvedPorts.inputs. This handles cases where ports were
397
+ * renamed or deleted but the old connection data persisted.
398
+ */
399
+ function cleanupStaleConnections({ nodes, }) {
400
+ return nodes.map(node => {
401
+ // Only compose-workflow nodes have resolvedPorts
402
+ if (node.type !== 'compose-workflow') {
403
+ return node;
404
+ }
405
+ const resolvedInputs = node.config.resolvedPorts?.inputs;
406
+ // If no resolvedPorts.inputs, we can't validate - skip cleanup for this node
407
+ if (!resolvedInputs || !Array.isArray(resolvedInputs)) {
408
+ return node;
409
+ }
410
+ const validInputIds = new Set(resolvedInputs.map(p => p.id));
411
+ const currentInputs = node.inputs ?? {};
412
+ // Check if any inputs are stale (reference non-existent ports)
413
+ const staleInputIds = Object.keys(currentInputs).filter(inputId => !validInputIds.has(inputId));
414
+ if (staleInputIds.length === 0) {
415
+ return node; // No cleanup needed
416
+ }
417
+ // Remove stale inputs
418
+ const cleanedInputs = { ...currentInputs };
419
+ staleInputIds.forEach(inputId => {
420
+ delete cleanedInputs[inputId];
421
+ });
422
+ return {
423
+ ...node,
424
+ inputs: cleanedInputs,
425
+ };
426
+ });
427
+ }
428
+ // ===========================================================================
429
+ // For-Each Context Validation
430
+ // ===========================================================================
431
+ /**
432
+ * Find the for-each context a node belongs to by tracing backwards.
433
+ *
434
+ * A node is "inside" a for-each context if, when tracing backwards through
435
+ * connections, we reach a for-each node via one of its OUTPUT ports.
436
+ *
437
+ * Returns the for-each node ID if inside a context, or null if outside all contexts.
438
+ */
439
+ function getForEachContext({ nodeId, nodes, }) {
440
+ const connections = deriveConnectionsInternal(nodes);
441
+ return getForEachContextInternal(nodeId, nodes, connections);
442
+ }
443
+ /**
444
+ * Check if a connection would violate for-each context boundaries.
445
+ *
446
+ * A violation occurs when:
447
+ * 1. Source is OUTSIDE a for-each context and target is INSIDE,
448
+ * AND the source would NOT become a "captured" node after this connection
449
+ * 2. Source and target are in DIFFERENT for-each contexts
450
+ * (can't connect across different loops)
451
+ *
452
+ * "Captured" nodes are allowed - they will be expanded at runtime to execute
453
+ * once per for-each iteration (e.g., Account selector → Auto Post inside for-each).
454
+ *
455
+ * Returns true if the connection is INVALID (violation), false if allowed.
456
+ */
457
+ function checkCrossContextViolation({ sourceNodeId, targetNodeId, nodes, }) {
458
+ const connections = deriveConnectionsInternal(nodes);
459
+ const targetNode = nodes.find(n => n.id === targetNodeId);
460
+ if (!targetNode)
461
+ return false;
462
+ // If target is a for-each node, the connection is going INTO the for-each
463
+ // (to its array input or passthrough inputs), which is always allowed
464
+ if (targetNode.type === 'for-each') {
465
+ return false;
466
+ }
467
+ const sourceNode = nodes.find(n => n.id === sourceNodeId);
468
+ const targetContext = getForEachContextInternal(targetNodeId, nodes, connections);
469
+ // If source IS the for-each node that defines the target's context,
470
+ // allow the connection (for-each feeding its own loop body via passthrough outputs)
471
+ if (sourceNode?.type === 'for-each' && targetContext === sourceNodeId) {
472
+ return false;
473
+ }
474
+ const sourceContext = getForEachContextInternal(sourceNodeId, nodes, connections);
475
+ // If source is outside and target is inside a for-each context...
476
+ if (sourceContext === null && targetContext !== null) {
477
+ // Check if source would become a "captured" node after this connection
478
+ const downstreamNodes = getDownstreamNodes(targetContext, nodes);
479
+ if (wouldBeCaptured(sourceNodeId, targetContext, downstreamNodes, nodes, targetNodeId)) {
480
+ // Source will be captured - allow the connection
481
+ return false;
482
+ }
483
+ // Source has other outputs outside the context - violation
484
+ return true;
485
+ }
486
+ // If both are inside different for-each contexts, that's a violation
487
+ if (sourceContext !== null && targetContext !== null && sourceContext !== targetContext) {
488
+ return true;
489
+ }
490
+ return false;
491
+ }
492
+ // ===========================================================================
493
+ // Workflow Validation
494
+ // ===========================================================================
495
+ const TRIGGER_TYPES = ['manual-trigger', 'recurrence'];
496
+ const TERMINAL_TYPES = ['passthrough', 'auto-post', 'save-to-media'];
497
+ /**
498
+ * Validate workflow and return structured errors for UI highlighting
499
+ */
500
+ async function validateWorkflow({ nodes, }) {
501
+ const errors = [];
502
+ const connections = deriveConnectionsInternal(nodes);
503
+ // 1. Check for trigger node
504
+ const hasTrigger = nodes.some(n => TRIGGER_TYPES.includes(n.type));
505
+ if (!hasTrigger) {
506
+ errors.push({
507
+ type: 'missing_trigger',
508
+ message: 'Automation requires a trigger node (Manual Trigger or Recurrence)',
509
+ });
510
+ }
511
+ // 2. Check for terminal node
512
+ const hasTerminal = nodes.some(n => TERMINAL_TYPES.includes(n.type));
513
+ if (!hasTerminal) {
514
+ errors.push({
515
+ type: 'missing_terminal',
516
+ message: 'Automation requires an output node (Output, Auto Post, or Save to Media)',
517
+ });
518
+ }
519
+ // Build connection lookup: targetNodeId-targetInputId -> connection
520
+ const connectionsByTarget = new Map();
521
+ connections.forEach(conn => {
522
+ connectionsByTarget.set(`${conn.targetNodeId}-${conn.targetInputId}`, conn);
523
+ });
524
+ // Build node lookup by ID
525
+ const nodeById = new Map();
526
+ nodes.forEach(n => nodeById.set(n.id, n));
527
+ // 3. Check each node for missing required inputs and type mismatches
528
+ for (const node of nodes) {
529
+ // Use node definition's computePorts if available
530
+ const definition = (0, nodes_1.getNodeDefinition)(node.type);
531
+ const inputPorts = definition
532
+ ? (await definition.computePorts({ config: node.config ?? {} })).inputs
533
+ : [];
534
+ for (const port of inputPorts) {
535
+ if (!port.required)
536
+ continue;
537
+ const connectionKey = `${node.id}-${port.id}`;
538
+ const connection = connectionsByTarget.get(connectionKey);
539
+ if (!connection) {
540
+ // Missing required input
541
+ errors.push({
542
+ type: 'missing_required_input',
543
+ nodeId: node.id,
544
+ portId: port.id,
545
+ message: `Required input '${port.id}' is not connected`,
546
+ });
547
+ }
548
+ else {
549
+ // Check type compatibility
550
+ const sourceNode = nodeById.get(connection.sourceNodeId);
551
+ if (sourceNode) {
552
+ const sourceDefinition = (0, nodes_1.getNodeDefinition)(sourceNode.type);
553
+ const sourceOutputs = sourceDefinition
554
+ ? (await sourceDefinition.computePorts({ config: sourceNode.config ?? {} })).outputs
555
+ : [];
556
+ const sourcePort = sourceOutputs.find(p => p.id === connection.sourceOutputId);
557
+ if (sourcePort) {
558
+ const compatible = areTypesCompatible({
559
+ sourceType: sourcePort.type,
560
+ sourceIsArray: sourcePort.isArray,
561
+ targetType: port.type,
562
+ targetIsArray: port.isArray,
563
+ });
564
+ if (!compatible) {
565
+ errors.push({
566
+ type: 'type_mismatch',
567
+ nodeId: node.id,
568
+ portId: port.id,
569
+ message: `Type mismatch: expected ${formatPortType(port.type, port.isArray)}, got ${formatPortType(sourcePort.type, sourcePort.isArray)}`,
570
+ });
571
+ }
572
+ }
573
+ }
574
+ }
575
+ }
576
+ // 4. Check for empty media pool
577
+ if (node.type === 'media') {
578
+ const configOutputs = node.config.outputs ?? [];
579
+ // Check if ANY output has media selected
580
+ const totalSelected = configOutputs.reduce((sum, o) => sum + o.selectedMediaIds.length, 0);
581
+ if (totalSelected === 0) {
582
+ errors.push({
583
+ type: 'empty_media_pool',
584
+ nodeId: node.id,
585
+ message: 'Media node has no media selected',
586
+ });
587
+ }
588
+ }
589
+ // 5. Check for empty account pool
590
+ if (node.type === 'account') {
591
+ const accountIds = node.config.accountIds ?? [];
592
+ if (accountIds.length === 0) {
593
+ errors.push({
594
+ type: 'empty_account_pool',
595
+ nodeId: node.id,
596
+ message: 'Account node has no accounts selected',
597
+ });
598
+ }
599
+ }
600
+ }
601
+ return errors;
602
+ }
603
+ /**
604
+ * Format a port type for display in error messages
605
+ */
606
+ function formatPortType(type, isArray) {
607
+ return (0, types_1.formatPortType)(type, isArray);
608
+ }
609
+ /**
610
+ * Get node IDs that have errors
611
+ */
612
+ function getErrorNodeIds({ errors }) {
613
+ return [...new Set(errors.filter(e => e.nodeId).map(e => e.nodeId))];
614
+ }
615
+ /**
616
+ * Get port errors for a specific node
617
+ */
618
+ function getPortErrorsForNode({ nodeId, errors, }) {
619
+ return errors
620
+ .filter(e => e.nodeId === nodeId && e.portId)
621
+ .map(e => ({ portId: e.portId, message: e.message }));
622
+ }
623
+ /**
624
+ * Check if there's a missing trigger error
625
+ */
626
+ function hasMissingTriggerError({ errors }) {
627
+ return errors.some(e => e.type === 'missing_trigger');
628
+ }
629
+ /**
630
+ * Check if there's a missing terminal error
631
+ */
632
+ function hasMissingTerminalError({ errors }) {
633
+ return errors.some(e => e.type === 'missing_terminal');
634
+ }
635
+ // ===========================================================================
636
+ // Preview Resolution
637
+ // ===========================================================================
638
+ /**
639
+ * Resolve preview data for a node.
640
+ * Converts node config with refs (inputId/textInputId) to render-ready format.
641
+ * Delegates to the node definition's resolvePreview function if available.
642
+ *
643
+ * @returns ResolvedPreview with render-ready config, sources, and textContent,
644
+ * or null if the node doesn't support preview or config is invalid.
645
+ */
646
+ function resolveNodePreview({ nodes, nodeId, }) {
647
+ const node = nodes.find(n => n.id === nodeId);
648
+ if (!node)
649
+ return null;
650
+ const definition = (0, nodes_1.getNodeDefinition)(node.type);
651
+ if (!definition)
652
+ return null;
653
+ if (!definition.resolvePreview)
654
+ return null;
655
+ return definition.resolvePreview(node.config);
656
+ }