Skip to content

Commit 1d2aa68

Browse files
author
drowl87
committed
enhance DynamicNode to support individual execution per item and add options for workflow execution control; bump version to 0.2.9
1 parent b3822b8 commit 1d2aa68

2 files changed

Lines changed: 77 additions & 80 deletions

File tree

nodes/DynamicNode/DynamicNode.node.ts

Lines changed: 73 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import {
77
NodeConnectionType,
88
} from 'n8n-workflow';
99
import { v4 as uuidv4 } from 'uuid';
10-
11-
// JSON import requires "resolveJsonModule" in tsconfig
1210
import subWorkflowTemplate from './subWorkflowTemplate.json';
1311

1412
export class DynamicNode implements INodeType {
@@ -30,108 +28,107 @@ export class DynamicNode implements INodeType {
3028
default: {},
3129
description: 'Paste in your exported node JSON here',
3230
},
31+
{
32+
displayName: 'Execute individually per item?',
33+
name: 'executeIndividually',
34+
type: 'boolean',
35+
default: true,
36+
description:
37+
'If enabled, each input item will run in its own sub-workflow. Disable only if your node can handle bulk items safely.',
38+
},
39+
{
40+
displayName: 'Disable waiting for child workflow(s) to finish?',
41+
name: 'doNotWaitToFinish',
42+
type: 'boolean',
43+
default: false,
44+
description:
45+
'⚠️ Advanced: If enabled, the parent will not wait for results from the sub-workflow. This may break downstream logic or lose returned data.',
46+
},
3347
],
3448
};
3549

3650
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
37-
// 1) Pull in the incoming items
38-
const items = this.getInputData();
51+
const inputItems = this.getInputData();
52+
const executeIndividually = this.getNodeParameter('executeIndividually', 0) as boolean;
53+
const doNotWaitToFinish = this.getNodeParameter('doNotWaitToFinish', 0) as boolean;
3954

40-
// 2) Get the user-provided node JSON (string or object)
4155
const rawParam = this.getNodeParameter('nodeJson', 0) as any;
4256
let raw: any;
4357
if (typeof rawParam === 'string') {
4458
try {
4559
raw = JSON.parse(rawParam);
4660
} catch {
47-
throw new NodeOperationError(
48-
this.getNode(),
49-
'Node JSON must be a valid JSON object or a parseable JSON string',
50-
);
61+
throw new NodeOperationError(this.getNode(), 'Node JSON must be valid JSON');
5162
}
5263
} else {
5364
raw = rawParam;
5465
}
66+
5567
if (typeof raw !== 'object' || raw === null) {
5668
throw new NodeOperationError(this.getNode(), 'Node JSON must be an object');
5769
}
5870

59-
// —— UNWRAP & CLEANUP ——
60-
// 3) If it’s a full export, pull out the first node
61-
let nodeJson: any;
62-
if (Array.isArray(raw.nodes) && raw.nodes.length > 0) {
63-
nodeJson = raw.nodes[0];
64-
} else {
65-
nodeJson = raw;
66-
}
67-
// 4) Remove export-only keys
68-
delete nodeJson.connections;
69-
delete nodeJson.pinData;
70-
delete nodeJson.meta;
71-
// 5) Validate it still has a name
72-
if (!nodeJson.name) {
71+
const baseNode = Array.isArray(raw.nodes) && raw.nodes.length > 0 ? raw.nodes[0] : raw;
72+
delete baseNode.connections;
73+
delete baseNode.pinData;
74+
delete baseNode.meta;
75+
76+
if (!baseNode.name) {
7377
throw new NodeOperationError(this.getNode(), 'Your JSON must include a `name` field');
7478
}
7579

76-
// —— AVOID COLLISIONS ——
77-
// 6) Suffix the name and assign a fresh ID
78-
nodeJson.name = `${nodeJson.name} - Dynamic Node`;
79-
nodeJson.id = `dynamic-${uuidv4()}`;
80+
const allResults: INodeExecutionData[] = [];
8081

81-
// —— DEFAULT POSITION ——
82-
// 7) Ensure a valid position array
83-
if (
84-
!nodeJson.position ||
85-
!Array.isArray(nodeJson.position) ||
86-
nodeJson.position.length !== 2 ||
87-
typeof nodeJson.position[0] !== 'number' ||
88-
typeof nodeJson.position[1] !== 'number'
89-
) {
90-
nodeJson.position = [240, 0];
91-
}
82+
const processItem = async (item: INodeExecutionData, index: number): Promise<void> => {
83+
const template = JSON.parse(JSON.stringify(subWorkflowTemplate));
84+
const nodeClone = JSON.parse(JSON.stringify(baseNode));
9285

93-
// 8) Clone the sub-workflow template
94-
const template = JSON.parse(JSON.stringify(subWorkflowTemplate)) as any;
86+
nodeClone.name = `${baseNode.name} - Dynamic Node [${index + 1}]`;
87+
nodeClone.id = `dynamic-${uuidv4()}`;
88+
nodeClone.position = Array.isArray(baseNode.position) && baseNode.position.length === 2
89+
? baseNode.position
90+
: [240, 0];
9591

96-
// 9) Inject & wire
97-
template.nodes.push(nodeJson);
98-
template.connections.Start.main[0][0].node = nodeJson.name;
92+
template.nodes.push(nodeClone);
93+
template.connections.Start.main[0][0].node = nodeClone.name;
9994

100-
// 10) Execute the mini-workflow to completion
101-
const workflowProxy = this.getWorkflowDataProxy(0);
102-
const executionResult: any = await this.executeWorkflow(
103-
{ code: template },
104-
items,
105-
{},
106-
{
107-
parentExecution: {
108-
executionId: workflowProxy.$execution.id,
109-
workflowId: workflowProxy.$workflow.id,
95+
const workflowProxy = this.getWorkflowDataProxy(index);
96+
97+
const execResult = await this.executeWorkflow(
98+
{ code: template },
99+
[item],
100+
{},
101+
{
102+
parentExecution: {
103+
executionId: workflowProxy.$execution.id,
104+
workflowId: workflowProxy.$workflow.id,
105+
},
106+
doNotWaitToFinish,
110107
},
111-
doNotWaitToFinish: false,
112-
},
113-
);
108+
);
114109

115-
// 11) Process executionResult
116-
let returnedData: INodeExecutionData[][] = [];
117-
if (Array.isArray(executionResult)) {
118-
returnedData = executionResult as INodeExecutionData[][];
119-
} else if (executionResult && typeof executionResult === 'object' && 'data' in executionResult) {
120-
if (Array.isArray((executionResult as any).data)) {
121-
returnedData = (executionResult as any).data as INodeExecutionData[][];
122-
} else {
123-
this.logger.warn('DynamicNode: Sub-workflow executionResult.data was not an array. Returning empty data.');
124-
returnedData = [];
125-
}
126-
} else if (executionResult === null || executionResult === undefined) {
127-
this.logger.warn('DynamicNode: Sub-workflow executionResult was null or undefined. Returning empty data.');
128-
returnedData = [];
129-
} else {
130-
// Catch-all for other unexpected structures from executeWorkflow
131-
this.logger.warn(`DynamicNode: Unexpected structure from sub-workflow execution. Type: ${typeof executionResult}. Returning empty data.`);
132-
returnedData = [];
133-
}
110+
if (!doNotWaitToFinish && execResult) {
111+
if (Array.isArray(execResult)) {
112+
allResults.push(...execResult.flat());
113+
} else if (typeof execResult === 'object' && 'data' in execResult && Array.isArray(execResult.data)) {
114+
allResults.push(...execResult.data.flat());
115+
}
116+
}
117+
};
118+
119+
if (executeIndividually) {
120+
for (let i = 0; i < inputItems.length; i++) {
121+
try {
122+
await processItem(inputItems[i], i);
123+
} catch (err) {
124+
this.logger.warn(`DynamicNode: Error processing item #${i + 1}: ${err.message}`);
125+
}
126+
}
127+
} else {
128+
// Run one sub-workflow with all items
129+
await processItem({ json: {} }, 0);
130+
}
134131

135-
return returnedData;
132+
return [allResults];
136133
}
137134
}

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "n8n-nodes-dynamic-node",
3-
"version": "0.2.8",
3+
"version": "0.2.9",
44
"description": "A dynamic n8n node wrapper that can execute any node JSON by feeding it at runtime.",
55
"keywords": [
66
"n8n-community-node-package",
@@ -48,11 +48,11 @@
4848
"globals": "^16.x",
4949
"typescript": "^5.x",
5050
"gulp": "^5.x",
51-
"prettier": "^3.x",
52-
"uuid": "^11.1.0"
51+
"prettier": "^3.x"
5352
},
5453
"dependencies": {
55-
"n8n-workflow": "^1.90.0"
54+
"n8n-workflow": "^1.90.0",
55+
"uuid": "^11.1.0"
5656
},
5757
"peerDependencies": {
5858
"n8n-workflow": "^1.90.0"

0 commit comments

Comments
 (0)