@pagelines/n8n-mcp 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/.github/workflows/ci.yml +38 -0
- package/README.md +155 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +192 -0
- package/dist/n8n-client.d.ts +54 -0
- package/dist/n8n-client.js +275 -0
- package/dist/n8n-client.test.d.ts +1 -0
- package/dist/n8n-client.test.js +184 -0
- package/dist/tools.d.ts +6 -0
- package/dist/tools.js +260 -0
- package/dist/types.d.ts +132 -0
- package/dist/types.js +5 -0
- package/dist/validators.d.ts +10 -0
- package/dist/validators.js +171 -0
- package/dist/validators.test.d.ts +1 -0
- package/dist/validators.test.js +148 -0
- package/package.json +42 -0
- package/server.json +58 -0
- package/src/index.ts +243 -0
- package/src/n8n-client.test.ts +227 -0
- package/src/n8n-client.ts +361 -0
- package/src/tools.ts +273 -0
- package/src/types.ts +107 -0
- package/src/validators.test.ts +180 -0
- package/src/validators.ts +208 -0
- package/tsconfig.json +15 -0
- package/vitest.config.ts +8 -0
package/src/tools.ts
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool Definitions
|
|
3
|
+
* Minimal, focused toolset for workflow management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
|
|
8
|
+
export const tools: Tool[] = [
|
|
9
|
+
// ─────────────────────────────────────────────────────────────
|
|
10
|
+
// Workflow Operations
|
|
11
|
+
// ─────────────────────────────────────────────────────────────
|
|
12
|
+
{
|
|
13
|
+
name: 'workflow_list',
|
|
14
|
+
description: 'List all workflows. Returns id, name, active status.',
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
active: {
|
|
19
|
+
type: 'boolean',
|
|
20
|
+
description: 'Filter by active status',
|
|
21
|
+
},
|
|
22
|
+
limit: {
|
|
23
|
+
type: 'number',
|
|
24
|
+
description: 'Max results (default 100)',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
name: 'workflow_get',
|
|
32
|
+
description: 'Get a workflow by ID. Returns full workflow with nodes, connections, settings.',
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
id: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
description: 'Workflow ID',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
required: ['id'],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
{
|
|
46
|
+
name: 'workflow_create',
|
|
47
|
+
description: 'Create a new workflow. Returns the created workflow.',
|
|
48
|
+
inputSchema: {
|
|
49
|
+
type: 'object',
|
|
50
|
+
properties: {
|
|
51
|
+
name: {
|
|
52
|
+
type: 'string',
|
|
53
|
+
description: 'Workflow name (use snake_case)',
|
|
54
|
+
},
|
|
55
|
+
nodes: {
|
|
56
|
+
type: 'array',
|
|
57
|
+
description: 'Array of node definitions',
|
|
58
|
+
items: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: {
|
|
61
|
+
name: { type: 'string' },
|
|
62
|
+
type: { type: 'string' },
|
|
63
|
+
typeVersion: { type: 'number' },
|
|
64
|
+
position: {
|
|
65
|
+
type: 'array',
|
|
66
|
+
items: { type: 'number' },
|
|
67
|
+
minItems: 2,
|
|
68
|
+
maxItems: 2,
|
|
69
|
+
},
|
|
70
|
+
parameters: { type: 'object' },
|
|
71
|
+
credentials: { type: 'object' },
|
|
72
|
+
},
|
|
73
|
+
required: ['name', 'type', 'typeVersion', 'position', 'parameters'],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
connections: {
|
|
77
|
+
type: 'object',
|
|
78
|
+
description: 'Connection map: { "NodeName": { "main": [[{ "node": "TargetNode", "type": "main", "index": 0 }]] } }',
|
|
79
|
+
},
|
|
80
|
+
settings: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
description: 'Workflow settings',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
required: ['name', 'nodes', 'connections'],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
{
|
|
90
|
+
name: 'workflow_update',
|
|
91
|
+
description: `Update a workflow using patch operations. ALWAYS preserves existing data.
|
|
92
|
+
|
|
93
|
+
Operations:
|
|
94
|
+
- addNode: Add a new node
|
|
95
|
+
- removeNode: Remove a node and its connections
|
|
96
|
+
- updateNode: Update node properties (INCLUDE ALL existing parameters to preserve them)
|
|
97
|
+
- addConnection: Connect two nodes
|
|
98
|
+
- removeConnection: Disconnect two nodes
|
|
99
|
+
- updateSettings: Update workflow settings
|
|
100
|
+
- updateName: Rename the workflow
|
|
101
|
+
|
|
102
|
+
Example: { "operations": [{ "type": "updateNode", "nodeName": "my_node", "properties": { "parameters": { ...allExistingParams, "newParam": "value" } } }] }`,
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: 'object',
|
|
105
|
+
properties: {
|
|
106
|
+
id: {
|
|
107
|
+
type: 'string',
|
|
108
|
+
description: 'Workflow ID',
|
|
109
|
+
},
|
|
110
|
+
operations: {
|
|
111
|
+
type: 'array',
|
|
112
|
+
description: 'Array of patch operations',
|
|
113
|
+
items: {
|
|
114
|
+
type: 'object',
|
|
115
|
+
properties: {
|
|
116
|
+
type: {
|
|
117
|
+
type: 'string',
|
|
118
|
+
enum: ['addNode', 'removeNode', 'updateNode', 'addConnection', 'removeConnection', 'updateSettings', 'updateName'],
|
|
119
|
+
},
|
|
120
|
+
// For addNode
|
|
121
|
+
node: { type: 'object' },
|
|
122
|
+
// For removeNode, updateNode
|
|
123
|
+
nodeName: { type: 'string' },
|
|
124
|
+
// For updateNode
|
|
125
|
+
properties: { type: 'object' },
|
|
126
|
+
// For connections
|
|
127
|
+
from: { type: 'string' },
|
|
128
|
+
to: { type: 'string' },
|
|
129
|
+
fromOutput: { type: 'number' },
|
|
130
|
+
toInput: { type: 'number' },
|
|
131
|
+
outputType: { type: 'string' },
|
|
132
|
+
inputType: { type: 'string' },
|
|
133
|
+
// For updateSettings
|
|
134
|
+
settings: { type: 'object' },
|
|
135
|
+
// For updateName
|
|
136
|
+
name: { type: 'string' },
|
|
137
|
+
},
|
|
138
|
+
required: ['type'],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
required: ['id', 'operations'],
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
{
|
|
147
|
+
name: 'workflow_delete',
|
|
148
|
+
description: 'Delete a workflow permanently.',
|
|
149
|
+
inputSchema: {
|
|
150
|
+
type: 'object',
|
|
151
|
+
properties: {
|
|
152
|
+
id: {
|
|
153
|
+
type: 'string',
|
|
154
|
+
description: 'Workflow ID',
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
required: ['id'],
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
{
|
|
162
|
+
name: 'workflow_activate',
|
|
163
|
+
description: 'Activate a workflow (enable triggers).',
|
|
164
|
+
inputSchema: {
|
|
165
|
+
type: 'object',
|
|
166
|
+
properties: {
|
|
167
|
+
id: {
|
|
168
|
+
type: 'string',
|
|
169
|
+
description: 'Workflow ID',
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
required: ['id'],
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
{
|
|
177
|
+
name: 'workflow_deactivate',
|
|
178
|
+
description: 'Deactivate a workflow (disable triggers).',
|
|
179
|
+
inputSchema: {
|
|
180
|
+
type: 'object',
|
|
181
|
+
properties: {
|
|
182
|
+
id: {
|
|
183
|
+
type: 'string',
|
|
184
|
+
description: 'Workflow ID',
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
required: ['id'],
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
{
|
|
192
|
+
name: 'workflow_execute',
|
|
193
|
+
description: 'Execute a workflow via its webhook trigger. Workflow must have a webhook node.',
|
|
194
|
+
inputSchema: {
|
|
195
|
+
type: 'object',
|
|
196
|
+
properties: {
|
|
197
|
+
id: {
|
|
198
|
+
type: 'string',
|
|
199
|
+
description: 'Workflow ID',
|
|
200
|
+
},
|
|
201
|
+
data: {
|
|
202
|
+
type: 'object',
|
|
203
|
+
description: 'Data to send to the webhook',
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
required: ['id'],
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
// ─────────────────────────────────────────────────────────────
|
|
211
|
+
// Execution Operations
|
|
212
|
+
// ─────────────────────────────────────────────────────────────
|
|
213
|
+
{
|
|
214
|
+
name: 'execution_list',
|
|
215
|
+
description: 'List workflow executions. Filter by workflow or status.',
|
|
216
|
+
inputSchema: {
|
|
217
|
+
type: 'object',
|
|
218
|
+
properties: {
|
|
219
|
+
workflowId: {
|
|
220
|
+
type: 'string',
|
|
221
|
+
description: 'Filter by workflow ID',
|
|
222
|
+
},
|
|
223
|
+
status: {
|
|
224
|
+
type: 'string',
|
|
225
|
+
enum: ['success', 'error', 'waiting'],
|
|
226
|
+
description: 'Filter by status',
|
|
227
|
+
},
|
|
228
|
+
limit: {
|
|
229
|
+
type: 'number',
|
|
230
|
+
description: 'Max results (default 20)',
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
{
|
|
237
|
+
name: 'execution_get',
|
|
238
|
+
description: 'Get execution details including run data and errors.',
|
|
239
|
+
inputSchema: {
|
|
240
|
+
type: 'object',
|
|
241
|
+
properties: {
|
|
242
|
+
id: {
|
|
243
|
+
type: 'string',
|
|
244
|
+
description: 'Execution ID',
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
required: ['id'],
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
// ─────────────────────────────────────────────────────────────
|
|
252
|
+
// Validation
|
|
253
|
+
// ─────────────────────────────────────────────────────────────
|
|
254
|
+
{
|
|
255
|
+
name: 'workflow_validate',
|
|
256
|
+
description: `Validate a workflow against best practices:
|
|
257
|
+
- snake_case naming
|
|
258
|
+
- Explicit node references (no $json)
|
|
259
|
+
- No hardcoded IDs
|
|
260
|
+
- No hardcoded secrets
|
|
261
|
+
- No orphan nodes`,
|
|
262
|
+
inputSchema: {
|
|
263
|
+
type: 'object',
|
|
264
|
+
properties: {
|
|
265
|
+
id: {
|
|
266
|
+
type: 'string',
|
|
267
|
+
description: 'Workflow ID to validate',
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
required: ['id'],
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
];
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* n8n API Types
|
|
3
|
+
* Minimal types for workflow management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface N8nNode {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
type: string;
|
|
10
|
+
typeVersion: number;
|
|
11
|
+
position: [number, number];
|
|
12
|
+
parameters: Record<string, unknown>;
|
|
13
|
+
credentials?: Record<string, { id: string; name: string }>;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
notes?: string;
|
|
16
|
+
notesInFlow?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface N8nConnection {
|
|
20
|
+
node: string;
|
|
21
|
+
type: string;
|
|
22
|
+
index: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface N8nConnections {
|
|
26
|
+
[nodeName: string]: {
|
|
27
|
+
[outputType: string]: N8nConnection[][];
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface N8nWorkflow {
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
active: boolean;
|
|
35
|
+
nodes: N8nNode[];
|
|
36
|
+
connections: N8nConnections;
|
|
37
|
+
settings?: Record<string, unknown>;
|
|
38
|
+
staticData?: Record<string, unknown>;
|
|
39
|
+
tags?: { id: string; name: string }[];
|
|
40
|
+
createdAt: string;
|
|
41
|
+
updatedAt: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface N8nWorkflowListItem {
|
|
45
|
+
id: string;
|
|
46
|
+
name: string;
|
|
47
|
+
active: boolean;
|
|
48
|
+
createdAt: string;
|
|
49
|
+
updatedAt: string;
|
|
50
|
+
tags?: { id: string; name: string }[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface N8nExecution {
|
|
54
|
+
id: string;
|
|
55
|
+
workflowId: string;
|
|
56
|
+
finished: boolean;
|
|
57
|
+
mode: string;
|
|
58
|
+
startedAt: string;
|
|
59
|
+
stoppedAt?: string;
|
|
60
|
+
status: 'success' | 'error' | 'waiting' | 'running';
|
|
61
|
+
data?: {
|
|
62
|
+
resultData?: {
|
|
63
|
+
runData?: Record<string, unknown>;
|
|
64
|
+
error?: { message: string };
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface N8nExecutionListItem {
|
|
70
|
+
id: string;
|
|
71
|
+
workflowId: string;
|
|
72
|
+
status: string;
|
|
73
|
+
startedAt: string;
|
|
74
|
+
stoppedAt?: string;
|
|
75
|
+
mode: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Patch operations for partial updates
|
|
79
|
+
export type PatchOperation =
|
|
80
|
+
| { type: 'addNode'; node: Omit<N8nNode, 'id'> & { id?: string } }
|
|
81
|
+
| { type: 'removeNode'; nodeName: string }
|
|
82
|
+
| { type: 'updateNode'; nodeName: string; properties: Partial<N8nNode> }
|
|
83
|
+
| { type: 'addConnection'; from: string; to: string; fromOutput?: number; toInput?: number; outputType?: string; inputType?: string }
|
|
84
|
+
| { type: 'removeConnection'; from: string; to: string; fromOutput?: number; toInput?: number; outputType?: string }
|
|
85
|
+
| { type: 'updateSettings'; settings: Record<string, unknown> }
|
|
86
|
+
| { type: 'updateName'; name: string }
|
|
87
|
+
| { type: 'activate' }
|
|
88
|
+
| { type: 'deactivate' };
|
|
89
|
+
|
|
90
|
+
// Validation
|
|
91
|
+
export interface ValidationWarning {
|
|
92
|
+
node?: string;
|
|
93
|
+
rule: string;
|
|
94
|
+
message: string;
|
|
95
|
+
severity: 'error' | 'warning' | 'info';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ValidationResult {
|
|
99
|
+
valid: boolean;
|
|
100
|
+
warnings: ValidationWarning[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// API response wrappers
|
|
104
|
+
export interface N8nListResponse<T> {
|
|
105
|
+
data: T[];
|
|
106
|
+
nextCursor?: string;
|
|
107
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { validateWorkflow, validatePartialUpdate } from './validators.js';
|
|
3
|
+
import type { N8nWorkflow } from './types.js';
|
|
4
|
+
|
|
5
|
+
const createWorkflow = (overrides: Partial<N8nWorkflow> = {}): N8nWorkflow => ({
|
|
6
|
+
id: '1',
|
|
7
|
+
name: 'test_workflow',
|
|
8
|
+
active: false,
|
|
9
|
+
nodes: [],
|
|
10
|
+
connections: {},
|
|
11
|
+
createdAt: '2024-01-01',
|
|
12
|
+
updatedAt: '2024-01-01',
|
|
13
|
+
...overrides,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('validateWorkflow', () => {
|
|
17
|
+
it('passes for valid snake_case workflow', () => {
|
|
18
|
+
const workflow = createWorkflow({
|
|
19
|
+
name: 'my_workflow',
|
|
20
|
+
nodes: [
|
|
21
|
+
{
|
|
22
|
+
id: '1',
|
|
23
|
+
name: 'webhook_trigger',
|
|
24
|
+
type: 'n8n-nodes-base.webhook',
|
|
25
|
+
typeVersion: 1,
|
|
26
|
+
position: [0, 0],
|
|
27
|
+
parameters: { path: 'test' },
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const result = validateWorkflow(workflow);
|
|
33
|
+
expect(result.valid).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('warns on non-snake_case workflow name', () => {
|
|
37
|
+
const workflow = createWorkflow({ name: 'My-Workflow-Name' });
|
|
38
|
+
const result = validateWorkflow(workflow);
|
|
39
|
+
|
|
40
|
+
expect(result.warnings).toContainEqual(
|
|
41
|
+
expect.objectContaining({
|
|
42
|
+
rule: 'snake_case',
|
|
43
|
+
severity: 'warning',
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('warns on $json usage', () => {
|
|
49
|
+
const workflow = createWorkflow({
|
|
50
|
+
nodes: [
|
|
51
|
+
{
|
|
52
|
+
id: '1',
|
|
53
|
+
name: 'set_node',
|
|
54
|
+
type: 'n8n-nodes-base.set',
|
|
55
|
+
typeVersion: 1,
|
|
56
|
+
position: [0, 0],
|
|
57
|
+
parameters: { value: '={{ $json.field }}' },
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const result = validateWorkflow(workflow);
|
|
63
|
+
expect(result.warnings).toContainEqual(
|
|
64
|
+
expect.objectContaining({
|
|
65
|
+
rule: 'explicit_reference',
|
|
66
|
+
severity: 'warning',
|
|
67
|
+
})
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('errors on hardcoded secrets', () => {
|
|
72
|
+
const workflow = createWorkflow({
|
|
73
|
+
nodes: [
|
|
74
|
+
{
|
|
75
|
+
id: '1',
|
|
76
|
+
name: 'http_node',
|
|
77
|
+
type: 'n8n-nodes-base.httpRequest',
|
|
78
|
+
typeVersion: 1,
|
|
79
|
+
position: [0, 0],
|
|
80
|
+
parameters: { apiKey: 'sk_live_abc123def456' },
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const result = validateWorkflow(workflow);
|
|
86
|
+
expect(result.valid).toBe(false);
|
|
87
|
+
expect(result.warnings).toContainEqual(
|
|
88
|
+
expect.objectContaining({
|
|
89
|
+
rule: 'no_hardcoded_secrets',
|
|
90
|
+
severity: 'error',
|
|
91
|
+
})
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('warns on orphan nodes', () => {
|
|
96
|
+
const workflow = createWorkflow({
|
|
97
|
+
nodes: [
|
|
98
|
+
{
|
|
99
|
+
id: '1',
|
|
100
|
+
name: 'orphan_node',
|
|
101
|
+
type: 'n8n-nodes-base.set',
|
|
102
|
+
typeVersion: 1,
|
|
103
|
+
position: [0, 0],
|
|
104
|
+
parameters: {},
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
connections: {},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const result = validateWorkflow(workflow);
|
|
111
|
+
expect(result.warnings).toContainEqual(
|
|
112
|
+
expect.objectContaining({
|
|
113
|
+
rule: 'orphan_node',
|
|
114
|
+
severity: 'warning',
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('validatePartialUpdate', () => {
|
|
121
|
+
it('errors when node not found', () => {
|
|
122
|
+
const workflow = createWorkflow();
|
|
123
|
+
const warnings = validatePartialUpdate(workflow, 'nonexistent', {});
|
|
124
|
+
|
|
125
|
+
expect(warnings).toContainEqual(
|
|
126
|
+
expect.objectContaining({
|
|
127
|
+
rule: 'node_exists',
|
|
128
|
+
severity: 'error',
|
|
129
|
+
})
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('errors on parameter loss', () => {
|
|
134
|
+
const workflow = createWorkflow({
|
|
135
|
+
nodes: [
|
|
136
|
+
{
|
|
137
|
+
id: '1',
|
|
138
|
+
name: 'my_node',
|
|
139
|
+
type: 'n8n-nodes-base.set',
|
|
140
|
+
typeVersion: 1,
|
|
141
|
+
position: [0, 0],
|
|
142
|
+
parameters: { existingParam: 'value', anotherParam: 'value2' },
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const warnings = validatePartialUpdate(workflow, 'my_node', {
|
|
148
|
+
newParam: 'value', // Missing existingParam and anotherParam
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(warnings).toContainEqual(
|
|
152
|
+
expect.objectContaining({
|
|
153
|
+
rule: 'parameter_preservation',
|
|
154
|
+
severity: 'error',
|
|
155
|
+
})
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('passes when all parameters preserved', () => {
|
|
160
|
+
const workflow = createWorkflow({
|
|
161
|
+
nodes: [
|
|
162
|
+
{
|
|
163
|
+
id: '1',
|
|
164
|
+
name: 'my_node',
|
|
165
|
+
type: 'n8n-nodes-base.set',
|
|
166
|
+
typeVersion: 1,
|
|
167
|
+
position: [0, 0],
|
|
168
|
+
parameters: { existingParam: 'value' },
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const warnings = validatePartialUpdate(workflow, 'my_node', {
|
|
174
|
+
existingParam: 'value',
|
|
175
|
+
newParam: 'new',
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(warnings).toHaveLength(0);
|
|
179
|
+
});
|
|
180
|
+
});
|