mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-04-03 08:03:07 +00:00
feat: add patchNodeField operation for surgical string edits (v2.46.0) (#698)
Add dedicated `patchNodeField` operation to `n8n_update_partial_workflow` for surgical find/replace edits in node string fields. Strict alternative to the existing `__patch_find_replace` in updateNode — errors on not-found, detects ambiguous matches, supports replaceAll and regex flags. Security hardening: - Prototype pollution protection in setNestedProperty/getNestedProperty - ReDoS protection rejecting unsafe regex patterns (nested quantifiers) - Resource limits: max 50 patches, 500-char regex, 512KB field size Fixes #696 Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
ca20586eda
commit
2d4115530c
19
CHANGELOG.md
19
CHANGELOG.md
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2.46.0] - 2026-04-03
|
||||
|
||||
### Added
|
||||
|
||||
- **`patchNodeField` operation for `n8n_update_partial_workflow`** — a dedicated, strict find/replace operation for surgical string edits in node fields (Fixes #696). Key features:
|
||||
- **Strict error handling**: errors if find string not found (unlike `__patch_find_replace` which only warns)
|
||||
- **Ambiguity detection**: errors if find matches multiple times unless `replaceAll: true` is set
|
||||
- **`replaceAll` flag**: replace all occurrences of a string in a single patch
|
||||
- **`regex` flag**: use regex patterns for advanced find/replace
|
||||
- Top-level operation type for better discoverability
|
||||
|
||||
### Security
|
||||
|
||||
- **Prototype pollution protection** — `setNestedProperty` and `getNestedProperty` now reject paths containing `__proto__`, `constructor`, or `prototype`. Protects both `patchNodeField` and `updateNode` operations
|
||||
- **ReDoS protection** — regex patterns with nested quantifiers or overlapping alternations are rejected to prevent catastrophic backtracking
|
||||
- **Resource limits** — max 50 patches per operation, max 500-char regex patterns, max 512KB field size for regex operations
|
||||
|
||||
Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
|
||||
|
||||
## [2.45.1] - 2026-04-02
|
||||
|
||||
### Fixed
|
||||
|
||||
2
dist/mcp/handlers-n8n-manager.d.ts.map
vendored
2
dist/mcp/handlers-n8n-manager.d.ts.map
vendored
@@ -1 +1 @@
|
||||
{"version":3,"file":"handlers-n8n-manager.d.ts","sourceRoot":"","sources":["../../src/mcp/handlers-n8n-manager.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"handlers-n8n-manager.d.ts","sourceRoot":"","sources":["../../src/mcp/handlers-n8n-manager.ts"],"names":[],"mappings":"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"}
|
||||
18
dist/mcp/handlers-n8n-manager.js
vendored
18
dist/mcp/handlers-n8n-manager.js
vendored
@@ -519,6 +519,24 @@ async function handleUpdateWorkflow(args, repository, context) {
|
||||
if (updateData.nodes || updateData.connections) {
|
||||
const current = await client.getWorkflow(id);
|
||||
workflowBefore = JSON.parse(JSON.stringify(current));
|
||||
if (updateData.nodes && current.nodes) {
|
||||
const currentById = new Map();
|
||||
const currentByName = new Map();
|
||||
for (const node of current.nodes) {
|
||||
if (node.id)
|
||||
currentById.set(node.id, node);
|
||||
currentByName.set(node.name, node);
|
||||
}
|
||||
for (const node of updateData.nodes) {
|
||||
const hasCredentials = node.credentials && typeof node.credentials === 'object' && Object.keys(node.credentials).length > 0;
|
||||
if (!hasCredentials) {
|
||||
const match = (node.id && currentById.get(node.id)) || currentByName.get(node.name);
|
||||
if (match?.credentials) {
|
||||
node.credentials = match.credentials;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (createBackup !== false) {
|
||||
try {
|
||||
const versioningService = new workflow_versioning_service_1.WorkflowVersioningService(repository, client);
|
||||
|
||||
2
dist/mcp/handlers-n8n-manager.js.map
vendored
2
dist/mcp/handlers-n8n-manager.js.map
vendored
File diff suppressed because one or more lines are too long
2
dist/mcp/handlers-workflow-diff.d.ts.map
vendored
2
dist/mcp/handlers-workflow-diff.d.ts.map
vendored
@@ -1 +1 @@
|
||||
{"version":3,"file":"handlers-workflow-diff.d.ts","sourceRoot":"","sources":["../../src/mcp/handlers-workflow-diff.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAMnD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAE5D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAkF7D,wBAAsB,2BAA2B,CAC/C,IAAI,EAAE,OAAO,EACb,UAAU,EAAE,cAAc,EAC1B,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,eAAe,CAAC,CAib1B"}
|
||||
{"version":3,"file":"handlers-workflow-diff.d.ts","sourceRoot":"","sources":["../../src/mcp/handlers-workflow-diff.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAMnD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAE5D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAoF7D,wBAAsB,2BAA2B,CAC/C,IAAI,EAAE,OAAO,EACb,UAAU,EAAE,cAAc,EAC1B,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,eAAe,CAAC,CAib1B"}
|
||||
10
dist/mcp/handlers-workflow-diff.js
vendored
10
dist/mcp/handlers-workflow-diff.js
vendored
@@ -51,7 +51,7 @@ function getValidator(repository) {
|
||||
return cachedValidator;
|
||||
}
|
||||
const NODE_TARGETING_OPERATIONS = new Set([
|
||||
'updateNode', 'removeNode', 'moveNode', 'enableNode', 'disableNode'
|
||||
'updateNode', 'removeNode', 'moveNode', 'enableNode', 'disableNode', 'patchNodeField'
|
||||
]);
|
||||
const workflowDiffSchema = zod_1.z.object({
|
||||
id: zod_1.z.string(),
|
||||
@@ -62,6 +62,8 @@ const workflowDiffSchema = zod_1.z.object({
|
||||
nodeId: zod_1.z.string().optional(),
|
||||
nodeName: zod_1.z.string().optional(),
|
||||
updates: zod_1.z.any().optional(),
|
||||
fieldPath: zod_1.z.string().optional(),
|
||||
patches: zod_1.z.any().optional(),
|
||||
position: zod_1.z.tuple([zod_1.z.number(), zod_1.z.number()]).optional(),
|
||||
source: zod_1.z.string().optional(),
|
||||
target: zod_1.z.string().optional(),
|
||||
@@ -506,6 +508,8 @@ function inferIntentFromOperations(operations) {
|
||||
return `Remove node ${op.nodeName || op.nodeId || ''}`.trim();
|
||||
case 'updateNode':
|
||||
return `Update node ${op.nodeName || op.nodeId || ''}`.trim();
|
||||
case 'patchNodeField':
|
||||
return `Patch field on node ${op.nodeName || op.nodeId || ''}`.trim();
|
||||
case 'addConnection':
|
||||
return `Connect ${op.source || 'node'} to ${op.target || 'node'}`;
|
||||
case 'removeConnection':
|
||||
@@ -538,6 +542,10 @@ function inferIntentFromOperations(operations) {
|
||||
const count = opTypes.filter((t) => t === 'updateNode').length;
|
||||
summary.push(`update ${count} node${count > 1 ? 's' : ''}`);
|
||||
}
|
||||
if (typeSet.has('patchNodeField')) {
|
||||
const count = opTypes.filter((t) => t === 'patchNodeField').length;
|
||||
summary.push(`patch ${count} field${count > 1 ? 's' : ''}`);
|
||||
}
|
||||
if (typeSet.has('addConnection') || typeSet.has('rewireConnection')) {
|
||||
summary.push('modify connections');
|
||||
}
|
||||
|
||||
2
dist/mcp/handlers-workflow-diff.js.map
vendored
2
dist/mcp/handlers-workflow-diff.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"version":3,"file":"n8n-update-partial-workflow.d.ts","sourceRoot":"","sources":["../../../../src/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE7C,eAAO,MAAM,2BAA2B,EAAE,iBA2azC,CAAC"}
|
||||
{"version":3,"file":"n8n-update-partial-workflow.d.ts","sourceRoot":"","sources":["../../../../src/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE7C,eAAO,MAAM,2BAA2B,EAAE,iBA0bzC,CAAC"}
|
||||
@@ -5,7 +5,7 @@ exports.n8nUpdatePartialWorkflowDoc = {
|
||||
name: 'n8n_update_partial_workflow',
|
||||
category: 'workflow_management',
|
||||
essentials: {
|
||||
description: 'Update workflow incrementally with diff operations. Types: addNode, removeNode, updateNode, moveNode, enable/disableNode, addConnection, removeConnection, rewireConnection, cleanStaleConnections, replaceConnections, updateSettings, updateName, add/removeTag, activateWorkflow, deactivateWorkflow, transferWorkflow. Supports smart parameters (branch, case) for multi-output nodes. Full support for AI connections (ai_languageModel, ai_tool, ai_memory, ai_embedding, ai_vectorStore, ai_document, ai_textSplitter, ai_outputParser).',
|
||||
description: 'Update workflow incrementally with diff operations. Types: addNode, removeNode, updateNode, patchNodeField, moveNode, enable/disableNode, addConnection, removeConnection, rewireConnection, cleanStaleConnections, replaceConnections, updateSettings, updateName, add/removeTag, activateWorkflow, deactivateWorkflow, transferWorkflow. Supports smart parameters (branch, case) for multi-output nodes. Full support for AI connections (ai_languageModel, ai_tool, ai_memory, ai_embedding, ai_vectorStore, ai_document, ai_textSplitter, ai_outputParser).',
|
||||
keyParameters: ['id', 'operations', 'continueOnError'],
|
||||
example: 'n8n_update_partial_workflow({id: "wf_123", operations: [{type: "rewireConnection", source: "IF", from: "Old", to: "New", branch: "true"}]})',
|
||||
performance: 'Fast (50-200ms)',
|
||||
@@ -28,14 +28,15 @@ exports.n8nUpdatePartialWorkflowDoc = {
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: `Updates workflows using surgical diff operations instead of full replacement. Supports 17 operation types for precise modifications. Operations are validated and applied atomically by default - all succeed or none are applied.
|
||||
description: `Updates workflows using surgical diff operations instead of full replacement. Supports 18 operation types for precise modifications. Operations are validated and applied atomically by default - all succeed or none are applied.
|
||||
|
||||
## Available Operations:
|
||||
|
||||
### Node Operations (6 types):
|
||||
### Node Operations (7 types):
|
||||
- **addNode**: Add a new node with name, type, and position (required)
|
||||
- **removeNode**: Remove a node by ID or name
|
||||
- **updateNode**: Update node properties using dot notation (e.g., 'parameters.url')
|
||||
- **patchNodeField**: Surgically edit string fields using find/replace patches. Strict mode: errors if find string not found, errors if multiple matches (ambiguity) unless replaceAll is set. Supports replaceAll and regex flags.
|
||||
- **moveNode**: Change node position [x, y]
|
||||
- **enableNode**: Enable a disabled node
|
||||
- **disableNode**: Disable an active node
|
||||
@@ -336,6 +337,11 @@ n8n_update_partial_workflow({
|
||||
'// Validate before applying\nn8n_update_partial_workflow({id: "bcd", operations: [{type: "removeNode", nodeName: "Old Process"}], validateOnly: true})',
|
||||
'// Surgically edit code using __patch_find_replace (avoids replacing entire code block)\nn8n_update_partial_workflow({id: "pfr1", operations: [{type: "updateNode", nodeName: "Code", updates: {"parameters.jsCode": {"__patch_find_replace": [{"find": "const limit = 10;", "replace": "const limit = 50;"}]}}}]})',
|
||||
'// Multiple sequential patches on the same property\nn8n_update_partial_workflow({id: "pfr2", operations: [{type: "updateNode", nodeName: "Code", updates: {"parameters.jsCode": {"__patch_find_replace": [{"find": "api.old-domain.com", "replace": "api.new-domain.com"}, {"find": "Authorization: Bearer old_token", "replace": "Authorization: Bearer new_token"}]}}}]})',
|
||||
'\n// ============ PATCHNODEFIELD EXAMPLES (strict find/replace) ============',
|
||||
'// Surgical code edit with patchNodeField (errors if not found)\nn8n_update_partial_workflow({id: "pnf1", operations: [{type: "patchNodeField", nodeName: "Code", fieldPath: "parameters.jsCode", patches: [{find: "const limit = 10;", replace: "const limit = 50;"}]}]})',
|
||||
'// Replace all occurrences of a string\nn8n_update_partial_workflow({id: "pnf2", operations: [{type: "patchNodeField", nodeName: "Code", fieldPath: "parameters.jsCode", patches: [{find: "api.old.com", replace: "api.new.com", replaceAll: true}]}]})',
|
||||
'// Multiple sequential patches\nn8n_update_partial_workflow({id: "pnf3", operations: [{type: "patchNodeField", nodeName: "Set Email", fieldPath: "parameters.assignments.assignments.6.value", patches: [{find: "© 2025 n8n-mcp", replace: "© 2026 n8n-mcp"}, {find: "<p>Unsubscribe</p>", replace: ""}]}]})',
|
||||
'// Regex-based replacement\nn8n_update_partial_workflow({id: "pnf4", operations: [{type: "patchNodeField", nodeName: "Code", fieldPath: "parameters.jsCode", patches: [{find: "const\\\\s+limit\\\\s*=\\\\s*\\\\d+", replace: "const limit = 100", regex: true}]}]})',
|
||||
'\n// ============ AI CONNECTION EXAMPLES ============',
|
||||
'// Connect language model to AI Agent\nn8n_update_partial_workflow({id: "ai1", operations: [{type: "addConnection", source: "OpenAI Chat Model", target: "AI Agent", sourceOutput: "ai_languageModel"}]})',
|
||||
'// Connect tool to AI Agent\nn8n_update_partial_workflow({id: "ai2", operations: [{type: "addConnection", source: "HTTP Request Tool", target: "AI Agent", sourceOutput: "ai_tool"}]})',
|
||||
@@ -374,7 +380,10 @@ n8n_update_partial_workflow({
|
||||
'Configure Vector Store retrieval systems',
|
||||
'Swap language models in existing AI workflows',
|
||||
'Batch-update AI tool connections',
|
||||
'Transfer workflows between team projects (enterprise)'
|
||||
'Transfer workflows between team projects (enterprise)',
|
||||
'Surgical string edits in email templates, code, or JSON bodies (patchNodeField)',
|
||||
'Fix typos or update URLs in large HTML content without re-transmitting the full string',
|
||||
'Bulk find/replace across node field content (replaceAll flag)'
|
||||
],
|
||||
performance: 'Very fast - typically 50-200ms. Much faster than full updates as only changes are processed.',
|
||||
bestPractices: [
|
||||
@@ -397,7 +406,10 @@ n8n_update_partial_workflow({
|
||||
'To remove properties, set them to null in the updates object',
|
||||
'When migrating from deprecated properties, remove the old property and add the new one in the same operation',
|
||||
'Use null to resolve mutual exclusivity validation errors between properties',
|
||||
'Batch multiple property removals in a single updateNode operation for efficiency'
|
||||
'Batch multiple property removals in a single updateNode operation for efficiency',
|
||||
'Prefer patchNodeField over __patch_find_replace for strict error handling — patchNodeField errors on not-found and detects ambiguous matches',
|
||||
'Use replaceAll: true in patchNodeField when you want to replace all occurrences of a string',
|
||||
'Use regex: true in patchNodeField for pattern-based replacements (e.g., whitespace-insensitive matching)'
|
||||
],
|
||||
pitfalls: [
|
||||
'**REQUIRES N8N_API_URL and N8N_API_KEY environment variables** - will not work without n8n API access',
|
||||
@@ -420,6 +432,9 @@ n8n_update_partial_workflow({
|
||||
'**Corrupted workflows beyond repair**: Workflows in paradoxical states (API returns corrupt, API rejects updates) cannot be fixed via API - must be recreated',
|
||||
'**__patch_find_replace for code edits**: Instead of replacing entire code blocks, use `{"parameters.jsCode": {"__patch_find_replace": [{"find": "old text", "replace": "new text"}]}}` to surgically edit string properties',
|
||||
'__patch_find_replace replaces the FIRST occurrence of each find string. Patches are applied sequentially — order matters',
|
||||
'**patchNodeField is strict**: it ERRORS if the find string is not found (unlike __patch_find_replace which only warns)',
|
||||
'**patchNodeField detects ambiguity**: if find matches multiple times, it ERRORS unless replaceAll: true is set',
|
||||
'When using regex: true in patchNodeField, escape special regex characters (., *, +, etc.) if you want literal matching',
|
||||
'To remove a property, set it to null in the updates object',
|
||||
'When properties are mutually exclusive (e.g., continueOnFail and onError), setting only the new property will fail - you must remove the old one with null',
|
||||
'Removing a required property may cause validation errors - check node documentation first',
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"n8n-update-partial-workflow.js","sourceRoot":"","sources":["../../../../src/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"n8n-update-partial-workflow.js","sourceRoot":"","sources":["../../../../src/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.ts"],"names":[],"mappings":"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"}
|
||||
2
dist/mcp/tools-n8n-manager.js
vendored
2
dist/mcp/tools-n8n-manager.js
vendored
@@ -141,7 +141,7 @@ exports.n8nManagementTools = [
|
||||
},
|
||||
{
|
||||
name: 'n8n_update_partial_workflow',
|
||||
description: `Update workflow incrementally with diff operations. Types: addNode, removeNode, updateNode, moveNode, enable/disableNode, addConnection, removeConnection, updateSettings, updateName, add/removeTag, activate/deactivateWorkflow, transferWorkflow. See tools_documentation("n8n_update_partial_workflow", "full") for details.`,
|
||||
description: `Update workflow incrementally with diff operations. Types: addNode, removeNode, updateNode, patchNodeField, moveNode, enable/disableNode, addConnection, removeConnection, updateSettings, updateName, add/removeTag, activate/deactivateWorkflow, transferWorkflow. See tools_documentation("n8n_update_partial_workflow", "full") for details.`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: true,
|
||||
|
||||
2
dist/mcp/tools-n8n-manager.js.map
vendored
2
dist/mcp/tools-n8n-manager.js.map
vendored
File diff suppressed because one or more lines are too long
2
dist/services/workflow-diff-engine.d.ts
vendored
2
dist/services/workflow-diff-engine.d.ts
vendored
@@ -14,6 +14,7 @@ export declare class WorkflowDiffEngine {
|
||||
private validateAddNode;
|
||||
private validateRemoveNode;
|
||||
private validateUpdateNode;
|
||||
private validatePatchNodeField;
|
||||
private validateMoveNode;
|
||||
private validateToggleNode;
|
||||
private validateAddConnection;
|
||||
@@ -22,6 +23,7 @@ export declare class WorkflowDiffEngine {
|
||||
private applyAddNode;
|
||||
private applyRemoveNode;
|
||||
private applyUpdateNode;
|
||||
private applyPatchNodeField;
|
||||
private applyMoveNode;
|
||||
private applyEnableNode;
|
||||
private applyDisableNode;
|
||||
|
||||
2
dist/services/workflow-diff-engine.d.ts.map
vendored
2
dist/services/workflow-diff-engine.d.ts.map
vendored
@@ -1 +1 @@
|
||||
{"version":3,"file":"workflow-diff-engine.d.ts","sourceRoot":"","sources":["../../src/services/workflow-diff-engine.ts"],"names":[],"mappings":"AAMA,OAAO,EAEL,mBAAmB,EACnB,kBAAkB,EAuBnB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,QAAQ,EAAoC,MAAM,kBAAkB,CAAC;AAY9E,qBAAa,kBAAkB;IAE7B,OAAO,CAAC,SAAS,CAAkC;IAEnD,OAAO,CAAC,QAAQ,CAAqC;IAErD,OAAO,CAAC,eAAe,CAAqB;IAE5C,OAAO,CAAC,gBAAgB,CAAqB;IAE7C,OAAO,CAAC,SAAS,CAAgB;IACjC,OAAO,CAAC,YAAY,CAAgB;IAEpC,OAAO,CAAC,mBAAmB,CAAqB;IAK1C,SAAS,CACb,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,kBAAkB,CAAC;IAgO9B,OAAO,CAAC,iBAAiB;IA0CzB,OAAO,CAAC,cAAc;IA4DtB,OAAO,CAAC,eAAe;IAwBvB,OAAO,CAAC,kBAAkB;IAuB1B,OAAO,CAAC,kBAAkB;IA6D1B,OAAO,CAAC,gBAAgB;IAQxB,OAAO,CAAC,kBAAkB;IAU1B,OAAO,CAAC,qBAAqB;IAkD7B,OAAO,CAAC,wBAAwB;IA6ChC,OAAO,CAAC,wBAAwB;IAmDhC,OAAO,CAAC,YAAY;IA4BpB,OAAO,CAAC,eAAe;IAwCvB,OAAO,CAAC,eAAe;IA6CvB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,gBAAgB;IAWxB,OAAO,CAAC,sBAAsB;IA0D9B,OAAO,CAAC,kBAAkB;IAiD1B,OAAO,CAAC,qBAAqB;IAuC7B,OAAO,CAAC,qBAAqB;IA0B7B,OAAO,CAAC,mBAAmB;IAW3B,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,WAAW;IAYnB,OAAO,CAAC,cAAc;IAatB,OAAO,CAAC,wBAAwB;IAchC,OAAO,CAAC,0BAA0B;IAMlC,OAAO,CAAC,qBAAqB;IAM7B,OAAO,CAAC,uBAAuB;IAO/B,OAAO,CAAC,wBAAwB;IAOhC,OAAO,CAAC,qBAAqB;IAK7B,OAAO,CAAC,6BAA6B;IAKrC,OAAO,CAAC,0BAA0B;IA0BlC,OAAO,CAAC,0BAA0B;IA+ElC,OAAO,CAAC,uBAAuB;IAe/B,OAAO,CAAC,0BAA0B;IAmElC,OAAO,CAAC,iBAAiB;IAkBzB,OAAO,CAAC,QAAQ;IAsChB,OAAO,CAAC,uBAAuB;IAW/B,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,iBAAiB;CAoB1B"}
|
||||
{"version":3,"file":"workflow-diff-engine.d.ts","sourceRoot":"","sources":["../../src/services/workflow-diff-engine.ts"],"names":[],"mappings":"AAMA,OAAO,EAEL,mBAAmB,EACnB,kBAAkB,EAwBnB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,QAAQ,EAAoC,MAAM,kBAAkB,CAAC;AA6D9E,qBAAa,kBAAkB;IAE7B,OAAO,CAAC,SAAS,CAAkC;IAEnD,OAAO,CAAC,QAAQ,CAAqC;IAErD,OAAO,CAAC,eAAe,CAAqB;IAE5C,OAAO,CAAC,gBAAgB,CAAqB;IAE7C,OAAO,CAAC,SAAS,CAAgB;IACjC,OAAO,CAAC,YAAY,CAAgB;IAEpC,OAAO,CAAC,mBAAmB,CAAqB;IAK1C,SAAS,CACb,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,kBAAkB,CAAC;IAgO9B,OAAO,CAAC,iBAAiB;IA4CzB,OAAO,CAAC,cAAc;IA+DtB,OAAO,CAAC,eAAe;IAwBvB,OAAO,CAAC,kBAAkB;IAuB1B,OAAO,CAAC,kBAAkB;IA6D1B,OAAO,CAAC,sBAAsB;IAuE9B,OAAO,CAAC,gBAAgB;IAQxB,OAAO,CAAC,kBAAkB;IAU1B,OAAO,CAAC,qBAAqB;IAkD7B,OAAO,CAAC,wBAAwB;IA6ChC,OAAO,CAAC,wBAAwB;IAmDhC,OAAO,CAAC,YAAY;IA4BpB,OAAO,CAAC,eAAe;IAwCvB,OAAO,CAAC,eAAe;IA6CvB,OAAO,CAAC,mBAAmB;IAgE3B,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,gBAAgB;IAWxB,OAAO,CAAC,sBAAsB;IA0D9B,OAAO,CAAC,kBAAkB;IAiD1B,OAAO,CAAC,qBAAqB;IAuC7B,OAAO,CAAC,qBAAqB;IA0B7B,OAAO,CAAC,mBAAmB;IAW3B,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,WAAW;IAYnB,OAAO,CAAC,cAAc;IAatB,OAAO,CAAC,wBAAwB;IAchC,OAAO,CAAC,0BAA0B;IAMlC,OAAO,CAAC,qBAAqB;IAM7B,OAAO,CAAC,uBAAuB;IAO/B,OAAO,CAAC,wBAAwB;IAOhC,OAAO,CAAC,qBAAqB;IAK7B,OAAO,CAAC,6BAA6B;IAKrC,OAAO,CAAC,0BAA0B;IA0BlC,OAAO,CAAC,0BAA0B;IA+ElC,OAAO,CAAC,uBAAuB;IAe/B,OAAO,CAAC,0BAA0B;IAmElC,OAAO,CAAC,iBAAiB;IAkBzB,OAAO,CAAC,QAAQ;IAsChB,OAAO,CAAC,uBAAuB;IAW/B,OAAO,CAAC,iBAAiB;IAWzB,OAAO,CAAC,iBAAiB;CAyB1B"}
|
||||
147
dist/services/workflow-diff-engine.js
vendored
147
dist/services/workflow-diff-engine.js
vendored
@@ -6,6 +6,39 @@ const logger_1 = require("../utils/logger");
|
||||
const node_sanitizer_1 = require("./node-sanitizer");
|
||||
const node_type_utils_1 = require("../utils/node-type-utils");
|
||||
const logger = new logger_1.Logger({ prefix: '[WorkflowDiffEngine]' });
|
||||
const PATCH_LIMITS = {
|
||||
MAX_PATCHES: 50,
|
||||
MAX_REGEX_LENGTH: 500,
|
||||
MAX_FIELD_SIZE_REGEX: 512 * 1024,
|
||||
};
|
||||
const DANGEROUS_PATH_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
||||
function isUnsafeRegex(pattern) {
|
||||
const nestedQuantifier = /\([^)]*[+*][^)]*\)[+*{]/;
|
||||
if (nestedQuantifier.test(pattern))
|
||||
return true;
|
||||
const overlappingAlternation = /\([^)]*\|[^)]*\)[+*{]/;
|
||||
if (overlappingAlternation.test(pattern)) {
|
||||
const match = pattern.match(/\(([^)]*)\|([^)]*)\)[+*{]/);
|
||||
if (match) {
|
||||
const [, left, right] = match;
|
||||
const broadClasses = ['.', '\\w', '\\d', '\\s', '\\S', '\\W', '\\D', '[^'];
|
||||
const leftHasBroad = broadClasses.some(c => left.includes(c));
|
||||
const rightHasBroad = broadClasses.some(c => right.includes(c));
|
||||
if (leftHasBroad && rightHasBroad)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function countOccurrences(str, search) {
|
||||
let count = 0;
|
||||
let pos = 0;
|
||||
while ((pos = str.indexOf(search, pos)) !== -1) {
|
||||
count++;
|
||||
pos += search.length;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
class WorkflowDiffEngine {
|
||||
constructor() {
|
||||
this.renameMap = new Map();
|
||||
@@ -25,7 +58,7 @@ class WorkflowDiffEngine {
|
||||
this.tagsToRemove = [];
|
||||
this.transferToProjectId = undefined;
|
||||
const workflowCopy = JSON.parse(JSON.stringify(workflow));
|
||||
const nodeOperationTypes = ['addNode', 'removeNode', 'updateNode', 'moveNode', 'enableNode', 'disableNode'];
|
||||
const nodeOperationTypes = ['addNode', 'removeNode', 'updateNode', 'patchNodeField', 'moveNode', 'enableNode', 'disableNode'];
|
||||
const nodeOperations = [];
|
||||
const otherOperations = [];
|
||||
request.operations.forEach((operation, index) => {
|
||||
@@ -213,6 +246,8 @@ class WorkflowDiffEngine {
|
||||
return this.validateRemoveNode(workflow, operation);
|
||||
case 'updateNode':
|
||||
return this.validateUpdateNode(workflow, operation);
|
||||
case 'patchNodeField':
|
||||
return this.validatePatchNodeField(workflow, operation);
|
||||
case 'moveNode':
|
||||
return this.validateMoveNode(workflow, operation);
|
||||
case 'enableNode':
|
||||
@@ -254,6 +289,9 @@ class WorkflowDiffEngine {
|
||||
case 'updateNode':
|
||||
this.applyUpdateNode(workflow, operation);
|
||||
break;
|
||||
case 'patchNodeField':
|
||||
this.applyPatchNodeField(workflow, operation);
|
||||
break;
|
||||
case 'moveNode':
|
||||
this.applyMoveNode(workflow, operation);
|
||||
break;
|
||||
@@ -375,6 +413,63 @@ class WorkflowDiffEngine {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
validatePatchNodeField(workflow, operation) {
|
||||
if (!operation.nodeId && !operation.nodeName) {
|
||||
return `patchNodeField requires either "nodeId" or "nodeName"`;
|
||||
}
|
||||
if (!operation.fieldPath || typeof operation.fieldPath !== 'string') {
|
||||
return `patchNodeField requires a "fieldPath" string (e.g., "parameters.jsCode")`;
|
||||
}
|
||||
const pathSegments = operation.fieldPath.split('.');
|
||||
if (pathSegments.some(k => DANGEROUS_PATH_KEYS.has(k))) {
|
||||
return `patchNodeField: fieldPath "${operation.fieldPath}" contains a forbidden key (__proto__, constructor, or prototype)`;
|
||||
}
|
||||
if (!Array.isArray(operation.patches) || operation.patches.length === 0) {
|
||||
return `patchNodeField requires a non-empty "patches" array of {find, replace} objects`;
|
||||
}
|
||||
if (operation.patches.length > PATCH_LIMITS.MAX_PATCHES) {
|
||||
return `patchNodeField: too many patches (${operation.patches.length}). Maximum is ${PATCH_LIMITS.MAX_PATCHES} per operation. Split into multiple operations if needed.`;
|
||||
}
|
||||
for (let i = 0; i < operation.patches.length; i++) {
|
||||
const patch = operation.patches[i];
|
||||
if (!patch || typeof patch.find !== 'string' || typeof patch.replace !== 'string') {
|
||||
return `Invalid patch entry at index ${i}: each entry must have "find" (string) and "replace" (string)`;
|
||||
}
|
||||
if (patch.find.length === 0) {
|
||||
return `Invalid patch entry at index ${i}: "find" must not be empty`;
|
||||
}
|
||||
if (patch.regex) {
|
||||
if (patch.find.length > PATCH_LIMITS.MAX_REGEX_LENGTH) {
|
||||
return `Regex pattern at patch index ${i} is too long (${patch.find.length} chars). Maximum is ${PATCH_LIMITS.MAX_REGEX_LENGTH} characters.`;
|
||||
}
|
||||
try {
|
||||
new RegExp(patch.find);
|
||||
}
|
||||
catch (e) {
|
||||
return `Invalid regex pattern at patch index ${i}: ${e instanceof Error ? e.message : 'invalid regex'}`;
|
||||
}
|
||||
if (isUnsafeRegex(patch.find)) {
|
||||
return `Potentially unsafe regex pattern at patch index ${i}: nested quantifiers or overlapping alternations can cause excessive backtracking. Simplify the pattern or use literal matching (regex: false).`;
|
||||
}
|
||||
}
|
||||
}
|
||||
const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
|
||||
if (!node) {
|
||||
return this.formatNodeNotFoundError(workflow, operation.nodeId || operation.nodeName || '', 'patchNodeField');
|
||||
}
|
||||
const currentValue = this.getNestedProperty(node, operation.fieldPath);
|
||||
if (currentValue === undefined) {
|
||||
return `Cannot apply patchNodeField to "${operation.fieldPath}": property does not exist on node "${node.name}"`;
|
||||
}
|
||||
if (typeof currentValue !== 'string') {
|
||||
return `Cannot apply patchNodeField to "${operation.fieldPath}": current value is ${typeof currentValue}, expected string`;
|
||||
}
|
||||
const hasRegex = operation.patches.some(p => p.regex);
|
||||
if (hasRegex && typeof currentValue === 'string' && currentValue.length > PATCH_LIMITS.MAX_FIELD_SIZE_REGEX) {
|
||||
return `Field "${operation.fieldPath}" is too large for regex operations (${Math.round(currentValue.length / 1024)}KB). Maximum is ${PATCH_LIMITS.MAX_FIELD_SIZE_REGEX / 1024}KB. Use literal matching (regex: false) for large fields.`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
validateMoveNode(workflow, operation) {
|
||||
const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
|
||||
if (!node) {
|
||||
@@ -586,6 +681,51 @@ class WorkflowDiffEngine {
|
||||
const sanitized = (0, node_sanitizer_1.sanitizeNode)(node);
|
||||
Object.assign(node, sanitized);
|
||||
}
|
||||
applyPatchNodeField(workflow, operation) {
|
||||
const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
|
||||
if (!node)
|
||||
return;
|
||||
this.modifiedNodeIds.add(node.id);
|
||||
let current = this.getNestedProperty(node, operation.fieldPath);
|
||||
for (let i = 0; i < operation.patches.length; i++) {
|
||||
const patch = operation.patches[i];
|
||||
if (patch.regex) {
|
||||
const globalRegex = new RegExp(patch.find, 'g');
|
||||
const matches = current.match(globalRegex);
|
||||
if (!matches || matches.length === 0) {
|
||||
throw new Error(`patchNodeField: regex pattern "${patch.find}" not found in "${operation.fieldPath}" (patch index ${i}). ` +
|
||||
`Use n8n_get_workflow to inspect the current value.`);
|
||||
}
|
||||
if (matches.length > 1 && !patch.replaceAll) {
|
||||
throw new Error(`patchNodeField: regex pattern "${patch.find}" matches ${matches.length} times in "${operation.fieldPath}" (patch index ${i}). ` +
|
||||
`Set "replaceAll": true to replace all occurrences, or refine the pattern to match exactly once.`);
|
||||
}
|
||||
const regex = patch.replaceAll ? globalRegex : new RegExp(patch.find);
|
||||
current = current.replace(regex, patch.replace);
|
||||
}
|
||||
else {
|
||||
const occurrences = countOccurrences(current, patch.find);
|
||||
if (occurrences === 0) {
|
||||
throw new Error(`patchNodeField: "${patch.find.substring(0, 80)}" not found in "${operation.fieldPath}" (patch index ${i}). ` +
|
||||
`Ensure the find string exactly matches the current content (including whitespace and newlines). ` +
|
||||
`Use n8n_get_workflow to inspect the current value.`);
|
||||
}
|
||||
if (occurrences > 1 && !patch.replaceAll) {
|
||||
throw new Error(`patchNodeField: "${patch.find.substring(0, 80)}" found ${occurrences} times in "${operation.fieldPath}" (patch index ${i}). ` +
|
||||
`Set "replaceAll": true to replace all occurrences, or use a more specific find string that matches exactly once.`);
|
||||
}
|
||||
if (patch.replaceAll) {
|
||||
current = current.split(patch.find).join(patch.replace);
|
||||
}
|
||||
else {
|
||||
current = current.replace(patch.find, patch.replace);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setNestedProperty(node, operation.fieldPath, current);
|
||||
const sanitized = (0, node_sanitizer_1.sanitizeNode)(node);
|
||||
Object.assign(node, sanitized);
|
||||
}
|
||||
applyMoveNode(workflow, operation) {
|
||||
const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
|
||||
if (!node)
|
||||
@@ -924,6 +1064,8 @@ class WorkflowDiffEngine {
|
||||
const keys = path.split('.');
|
||||
let current = obj;
|
||||
for (const key of keys) {
|
||||
if (DANGEROUS_PATH_KEYS.has(key))
|
||||
return undefined;
|
||||
if (current == null || typeof current !== 'object')
|
||||
return undefined;
|
||||
current = current[key];
|
||||
@@ -933,6 +1075,9 @@ class WorkflowDiffEngine {
|
||||
setNestedProperty(obj, path, value) {
|
||||
const keys = path.split('.');
|
||||
let current = obj;
|
||||
if (keys.some(k => DANGEROUS_PATH_KEYS.has(k))) {
|
||||
throw new Error(`Invalid property path: "${path}" contains a forbidden key`);
|
||||
}
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const key = keys[i];
|
||||
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {
|
||||
|
||||
2
dist/services/workflow-diff-engine.js.map
vendored
2
dist/services/workflow-diff-engine.js.map
vendored
File diff suppressed because one or more lines are too long
16
dist/types/workflow-diff.d.ts
vendored
16
dist/types/workflow-diff.d.ts
vendored
@@ -40,6 +40,18 @@ export interface DisableNodeOperation extends DiffOperation {
|
||||
nodeId?: string;
|
||||
nodeName?: string;
|
||||
}
|
||||
export interface PatchNodeFieldOperation extends DiffOperation {
|
||||
type: 'patchNodeField';
|
||||
nodeId?: string;
|
||||
nodeName?: string;
|
||||
fieldPath: string;
|
||||
patches: Array<{
|
||||
find: string;
|
||||
replace: string;
|
||||
replaceAll?: boolean;
|
||||
regex?: boolean;
|
||||
}>;
|
||||
}
|
||||
export interface AddConnectionOperation extends DiffOperation {
|
||||
type: 'addConnection';
|
||||
source: string;
|
||||
@@ -114,7 +126,7 @@ export interface ReplaceConnectionsOperation extends DiffOperation {
|
||||
};
|
||||
};
|
||||
}
|
||||
export type WorkflowDiffOperation = AddNodeOperation | RemoveNodeOperation | UpdateNodeOperation | MoveNodeOperation | EnableNodeOperation | DisableNodeOperation | AddConnectionOperation | RemoveConnectionOperation | RewireConnectionOperation | UpdateSettingsOperation | UpdateNameOperation | AddTagOperation | RemoveTagOperation | ActivateWorkflowOperation | DeactivateWorkflowOperation | CleanStaleConnectionsOperation | ReplaceConnectionsOperation | TransferWorkflowOperation;
|
||||
export type WorkflowDiffOperation = AddNodeOperation | RemoveNodeOperation | UpdateNodeOperation | PatchNodeFieldOperation | MoveNodeOperation | EnableNodeOperation | DisableNodeOperation | AddConnectionOperation | RemoveConnectionOperation | RewireConnectionOperation | UpdateSettingsOperation | UpdateNameOperation | AddTagOperation | RemoveTagOperation | ActivateWorkflowOperation | DeactivateWorkflowOperation | CleanStaleConnectionsOperation | ReplaceConnectionsOperation | TransferWorkflowOperation;
|
||||
export interface WorkflowDiffRequest {
|
||||
id: string;
|
||||
operations: WorkflowDiffOperation[];
|
||||
@@ -149,7 +161,7 @@ export interface NodeReference {
|
||||
id?: string;
|
||||
name?: string;
|
||||
}
|
||||
export declare function isNodeOperation(op: WorkflowDiffOperation): op is AddNodeOperation | RemoveNodeOperation | UpdateNodeOperation | MoveNodeOperation | EnableNodeOperation | DisableNodeOperation;
|
||||
export declare function isNodeOperation(op: WorkflowDiffOperation): op is AddNodeOperation | RemoveNodeOperation | UpdateNodeOperation | PatchNodeFieldOperation | MoveNodeOperation | EnableNodeOperation | DisableNodeOperation;
|
||||
export declare function isConnectionOperation(op: WorkflowDiffOperation): op is AddConnectionOperation | RemoveConnectionOperation | RewireConnectionOperation | CleanStaleConnectionsOperation | ReplaceConnectionsOperation;
|
||||
export declare function isMetadataOperation(op: WorkflowDiffOperation): op is UpdateSettingsOperation | UpdateNameOperation | AddTagOperation | RemoveTagOperation;
|
||||
//# sourceMappingURL=workflow-diff.d.ts.map
|
||||
2
dist/types/workflow-diff.d.ts.map
vendored
2
dist/types/workflow-diff.d.ts.map
vendored
@@ -1 +1 @@
|
||||
{"version":3,"file":"workflow-diff.d.ts","sourceRoot":"","sources":["../../src/types/workflow-diff.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"workflow-diff.d.ts","sourceRoot":"","sources":["../../src/types/workflow-diff.ts"],"names":[],"mappings":"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"}
|
||||
2
dist/types/workflow-diff.js
vendored
2
dist/types/workflow-diff.js
vendored
@@ -4,7 +4,7 @@ exports.isNodeOperation = isNodeOperation;
|
||||
exports.isConnectionOperation = isConnectionOperation;
|
||||
exports.isMetadataOperation = isMetadataOperation;
|
||||
function isNodeOperation(op) {
|
||||
return ['addNode', 'removeNode', 'updateNode', 'moveNode', 'enableNode', 'disableNode'].includes(op.type);
|
||||
return ['addNode', 'removeNode', 'updateNode', 'patchNodeField', 'moveNode', 'enableNode', 'disableNode'].includes(op.type);
|
||||
}
|
||||
function isConnectionOperation(op) {
|
||||
return ['addConnection', 'removeConnection', 'rewireConnection', 'cleanStaleConnections', 'replaceConnections'].includes(op.type);
|
||||
|
||||
2
dist/types/workflow-diff.js.map
vendored
2
dist/types/workflow-diff.js.map
vendored
@@ -1 +1 @@
|
||||
{"version":3,"file":"workflow-diff.js","sourceRoot":"","sources":["../../src/types/workflow-diff.ts"],"names":[],"mappings":";;AAkNA,0CAIC;AAED,sDAGC;AAED,kDAGC;AAdD,SAAgB,eAAe,CAAC,EAAyB;IAGvD,OAAO,CAAC,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;AAC5G,CAAC;AAED,SAAgB,qBAAqB,CAAC,EAAyB;IAE7D,OAAO,CAAC,eAAe,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,oBAAoB,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;AACpI,CAAC;AAED,SAAgB,mBAAmB,CAAC,EAAyB;IAE3D,OAAO,CAAC,gBAAgB,EAAE,YAAY,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;AACnF,CAAC"}
|
||||
{"version":3,"file":"workflow-diff.js","sourceRoot":"","sources":["../../src/types/workflow-diff.ts"],"names":[],"mappings":";;AAgOA,0CAIC;AAED,sDAGC;AAED,kDAGC;AAdD,SAAgB,eAAe,CAAC,EAAyB;IAGvD,OAAO,CAAC,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;AAC9H,CAAC;AAED,SAAgB,qBAAqB,CAAC,EAAyB;IAE7D,OAAO,CAAC,eAAe,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,oBAAoB,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;AACpI,CAAC;AAED,SAAgB,mBAAmB,CAAC,EAAyB;IAE3D,OAAO,CAAC,gBAAgB,EAAE,YAAY,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;AACnF,CAAC"}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "n8n-mcp",
|
||||
"version": "2.45.1",
|
||||
"version": "2.46.0",
|
||||
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "n8n-mcp-runtime",
|
||||
"version": "2.45.1",
|
||||
"version": "2.46.0",
|
||||
"description": "n8n MCP Server Runtime Dependencies Only",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -33,7 +33,7 @@ function getValidator(repository: NodeRepository): WorkflowValidator {
|
||||
|
||||
// Operation types that identify nodes by nodeId/nodeName
|
||||
const NODE_TARGETING_OPERATIONS = new Set([
|
||||
'updateNode', 'removeNode', 'moveNode', 'enableNode', 'disableNode'
|
||||
'updateNode', 'removeNode', 'moveNode', 'enableNode', 'disableNode', 'patchNodeField'
|
||||
]);
|
||||
|
||||
// Zod schema for the diff request
|
||||
@@ -47,6 +47,8 @@ const workflowDiffSchema = z.object({
|
||||
nodeId: z.string().optional(),
|
||||
nodeName: z.string().optional(),
|
||||
updates: z.any().optional(),
|
||||
fieldPath: z.string().optional(),
|
||||
patches: z.any().optional(),
|
||||
position: z.tuple([z.number(), z.number()]).optional(),
|
||||
// Connection operations
|
||||
source: z.string().optional(),
|
||||
@@ -569,6 +571,8 @@ function inferIntentFromOperations(operations: any[]): string {
|
||||
return `Remove node ${op.nodeName || op.nodeId || ''}`.trim();
|
||||
case 'updateNode':
|
||||
return `Update node ${op.nodeName || op.nodeId || ''}`.trim();
|
||||
case 'patchNodeField':
|
||||
return `Patch field on node ${op.nodeName || op.nodeId || ''}`.trim();
|
||||
case 'addConnection':
|
||||
return `Connect ${op.source || 'node'} to ${op.target || 'node'}`;
|
||||
case 'removeConnection':
|
||||
@@ -604,6 +608,10 @@ function inferIntentFromOperations(operations: any[]): string {
|
||||
const count = opTypes.filter((t) => t === 'updateNode').length;
|
||||
summary.push(`update ${count} node${count > 1 ? 's' : ''}`);
|
||||
}
|
||||
if (typeSet.has('patchNodeField')) {
|
||||
const count = opTypes.filter((t) => t === 'patchNodeField').length;
|
||||
summary.push(`patch ${count} field${count > 1 ? 's' : ''}`);
|
||||
}
|
||||
if (typeSet.has('addConnection') || typeSet.has('rewireConnection')) {
|
||||
summary.push('modify connections');
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ export const n8nUpdatePartialWorkflowDoc: ToolDocumentation = {
|
||||
name: 'n8n_update_partial_workflow',
|
||||
category: 'workflow_management',
|
||||
essentials: {
|
||||
description: 'Update workflow incrementally with diff operations. Types: addNode, removeNode, updateNode, moveNode, enable/disableNode, addConnection, removeConnection, rewireConnection, cleanStaleConnections, replaceConnections, updateSettings, updateName, add/removeTag, activateWorkflow, deactivateWorkflow, transferWorkflow. Supports smart parameters (branch, case) for multi-output nodes. Full support for AI connections (ai_languageModel, ai_tool, ai_memory, ai_embedding, ai_vectorStore, ai_document, ai_textSplitter, ai_outputParser).',
|
||||
description: 'Update workflow incrementally with diff operations. Types: addNode, removeNode, updateNode, patchNodeField, moveNode, enable/disableNode, addConnection, removeConnection, rewireConnection, cleanStaleConnections, replaceConnections, updateSettings, updateName, add/removeTag, activateWorkflow, deactivateWorkflow, transferWorkflow. Supports smart parameters (branch, case) for multi-output nodes. Full support for AI connections (ai_languageModel, ai_tool, ai_memory, ai_embedding, ai_vectorStore, ai_document, ai_textSplitter, ai_outputParser).',
|
||||
keyParameters: ['id', 'operations', 'continueOnError'],
|
||||
example: 'n8n_update_partial_workflow({id: "wf_123", operations: [{type: "rewireConnection", source: "IF", from: "Old", to: "New", branch: "true"}]})',
|
||||
performance: 'Fast (50-200ms)',
|
||||
@@ -27,14 +27,15 @@ export const n8nUpdatePartialWorkflowDoc: ToolDocumentation = {
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: `Updates workflows using surgical diff operations instead of full replacement. Supports 17 operation types for precise modifications. Operations are validated and applied atomically by default - all succeed or none are applied.
|
||||
description: `Updates workflows using surgical diff operations instead of full replacement. Supports 18 operation types for precise modifications. Operations are validated and applied atomically by default - all succeed or none are applied.
|
||||
|
||||
## Available Operations:
|
||||
|
||||
### Node Operations (6 types):
|
||||
### Node Operations (7 types):
|
||||
- **addNode**: Add a new node with name, type, and position (required)
|
||||
- **removeNode**: Remove a node by ID or name
|
||||
- **updateNode**: Update node properties using dot notation (e.g., 'parameters.url')
|
||||
- **patchNodeField**: Surgically edit string fields using find/replace patches. Strict mode: errors if find string not found, errors if multiple matches (ambiguity) unless replaceAll is set. Supports replaceAll and regex flags.
|
||||
- **moveNode**: Change node position [x, y]
|
||||
- **enableNode**: Enable a disabled node
|
||||
- **disableNode**: Disable an active node
|
||||
@@ -335,6 +336,11 @@ n8n_update_partial_workflow({
|
||||
'// Validate before applying\nn8n_update_partial_workflow({id: "bcd", operations: [{type: "removeNode", nodeName: "Old Process"}], validateOnly: true})',
|
||||
'// Surgically edit code using __patch_find_replace (avoids replacing entire code block)\nn8n_update_partial_workflow({id: "pfr1", operations: [{type: "updateNode", nodeName: "Code", updates: {"parameters.jsCode": {"__patch_find_replace": [{"find": "const limit = 10;", "replace": "const limit = 50;"}]}}}]})',
|
||||
'// Multiple sequential patches on the same property\nn8n_update_partial_workflow({id: "pfr2", operations: [{type: "updateNode", nodeName: "Code", updates: {"parameters.jsCode": {"__patch_find_replace": [{"find": "api.old-domain.com", "replace": "api.new-domain.com"}, {"find": "Authorization: Bearer old_token", "replace": "Authorization: Bearer new_token"}]}}}]})',
|
||||
'\n// ============ PATCHNODEFIELD EXAMPLES (strict find/replace) ============',
|
||||
'// Surgical code edit with patchNodeField (errors if not found)\nn8n_update_partial_workflow({id: "pnf1", operations: [{type: "patchNodeField", nodeName: "Code", fieldPath: "parameters.jsCode", patches: [{find: "const limit = 10;", replace: "const limit = 50;"}]}]})',
|
||||
'// Replace all occurrences of a string\nn8n_update_partial_workflow({id: "pnf2", operations: [{type: "patchNodeField", nodeName: "Code", fieldPath: "parameters.jsCode", patches: [{find: "api.old.com", replace: "api.new.com", replaceAll: true}]}]})',
|
||||
'// Multiple sequential patches\nn8n_update_partial_workflow({id: "pnf3", operations: [{type: "patchNodeField", nodeName: "Set Email", fieldPath: "parameters.assignments.assignments.6.value", patches: [{find: "© 2025 n8n-mcp", replace: "© 2026 n8n-mcp"}, {find: "<p>Unsubscribe</p>", replace: ""}]}]})',
|
||||
'// Regex-based replacement\nn8n_update_partial_workflow({id: "pnf4", operations: [{type: "patchNodeField", nodeName: "Code", fieldPath: "parameters.jsCode", patches: [{find: "const\\\\s+limit\\\\s*=\\\\s*\\\\d+", replace: "const limit = 100", regex: true}]}]})',
|
||||
'\n// ============ AI CONNECTION EXAMPLES ============',
|
||||
'// Connect language model to AI Agent\nn8n_update_partial_workflow({id: "ai1", operations: [{type: "addConnection", source: "OpenAI Chat Model", target: "AI Agent", sourceOutput: "ai_languageModel"}]})',
|
||||
'// Connect tool to AI Agent\nn8n_update_partial_workflow({id: "ai2", operations: [{type: "addConnection", source: "HTTP Request Tool", target: "AI Agent", sourceOutput: "ai_tool"}]})',
|
||||
@@ -373,7 +379,10 @@ n8n_update_partial_workflow({
|
||||
'Configure Vector Store retrieval systems',
|
||||
'Swap language models in existing AI workflows',
|
||||
'Batch-update AI tool connections',
|
||||
'Transfer workflows between team projects (enterprise)'
|
||||
'Transfer workflows between team projects (enterprise)',
|
||||
'Surgical string edits in email templates, code, or JSON bodies (patchNodeField)',
|
||||
'Fix typos or update URLs in large HTML content without re-transmitting the full string',
|
||||
'Bulk find/replace across node field content (replaceAll flag)'
|
||||
],
|
||||
performance: 'Very fast - typically 50-200ms. Much faster than full updates as only changes are processed.',
|
||||
bestPractices: [
|
||||
@@ -396,7 +405,10 @@ n8n_update_partial_workflow({
|
||||
'To remove properties, set them to null in the updates object',
|
||||
'When migrating from deprecated properties, remove the old property and add the new one in the same operation',
|
||||
'Use null to resolve mutual exclusivity validation errors between properties',
|
||||
'Batch multiple property removals in a single updateNode operation for efficiency'
|
||||
'Batch multiple property removals in a single updateNode operation for efficiency',
|
||||
'Prefer patchNodeField over __patch_find_replace for strict error handling — patchNodeField errors on not-found and detects ambiguous matches',
|
||||
'Use replaceAll: true in patchNodeField when you want to replace all occurrences of a string',
|
||||
'Use regex: true in patchNodeField for pattern-based replacements (e.g., whitespace-insensitive matching)'
|
||||
],
|
||||
pitfalls: [
|
||||
'**REQUIRES N8N_API_URL and N8N_API_KEY environment variables** - will not work without n8n API access',
|
||||
@@ -419,6 +431,9 @@ n8n_update_partial_workflow({
|
||||
'**Corrupted workflows beyond repair**: Workflows in paradoxical states (API returns corrupt, API rejects updates) cannot be fixed via API - must be recreated',
|
||||
'**__patch_find_replace for code edits**: Instead of replacing entire code blocks, use `{"parameters.jsCode": {"__patch_find_replace": [{"find": "old text", "replace": "new text"}]}}` to surgically edit string properties',
|
||||
'__patch_find_replace replaces the FIRST occurrence of each find string. Patches are applied sequentially — order matters',
|
||||
'**patchNodeField is strict**: it ERRORS if the find string is not found (unlike __patch_find_replace which only warns)',
|
||||
'**patchNodeField detects ambiguity**: if find matches multiple times, it ERRORS unless replaceAll: true is set',
|
||||
'When using regex: true in patchNodeField, escape special regex characters (., *, +, etc.) if you want literal matching',
|
||||
'To remove a property, set it to null in the updates object',
|
||||
'When properties are mutually exclusive (e.g., continueOnFail and onError), setting only the new property will fail - you must remove the old one with null',
|
||||
'Removing a required property may cause validation errors - check node documentation first',
|
||||
|
||||
@@ -147,7 +147,7 @@ export const n8nManagementTools: ToolDefinition[] = [
|
||||
},
|
||||
{
|
||||
name: 'n8n_update_partial_workflow',
|
||||
description: `Update workflow incrementally with diff operations. Types: addNode, removeNode, updateNode, moveNode, enable/disableNode, addConnection, removeConnection, updateSettings, updateName, add/removeTag, activate/deactivateWorkflow, transferWorkflow. See tools_documentation("n8n_update_partial_workflow", "full") for details.`,
|
||||
description: `Update workflow incrementally with diff operations. Types: addNode, removeNode, updateNode, patchNodeField, moveNode, enable/disableNode, addConnection, removeConnection, updateSettings, updateName, add/removeTag, activate/deactivateWorkflow, transferWorkflow. See tools_documentation("n8n_update_partial_workflow", "full") for details.`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: true, // Allow any extra properties Claude Desktop might add
|
||||
|
||||
@@ -29,7 +29,8 @@ import {
|
||||
DeactivateWorkflowOperation,
|
||||
CleanStaleConnectionsOperation,
|
||||
ReplaceConnectionsOperation,
|
||||
TransferWorkflowOperation
|
||||
TransferWorkflowOperation,
|
||||
PatchNodeFieldOperation
|
||||
} from '../types/workflow-diff';
|
||||
import { Workflow, WorkflowNode, WorkflowConnection } from '../types/n8n-api';
|
||||
import { Logger } from '../utils/logger';
|
||||
@@ -39,6 +40,55 @@ import { isActivatableTrigger } from '../utils/node-type-utils';
|
||||
|
||||
const logger = new Logger({ prefix: '[WorkflowDiffEngine]' });
|
||||
|
||||
// Safety limits for patchNodeField operations
|
||||
const PATCH_LIMITS = {
|
||||
MAX_PATCHES: 50, // Max patches per operation
|
||||
MAX_REGEX_LENGTH: 500, // Max regex pattern length (chars)
|
||||
MAX_FIELD_SIZE_REGEX: 512 * 1024, // Max field size for regex operations (512KB)
|
||||
};
|
||||
|
||||
// Keys that must never appear in property paths (prototype pollution prevention)
|
||||
const DANGEROUS_PATH_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
||||
|
||||
/**
|
||||
* Check if a regex pattern contains constructs known to cause catastrophic backtracking.
|
||||
* Detects nested quantifiers like (a+)+, (a*)+, (a+)*, (a|b+)+ etc.
|
||||
*/
|
||||
function isUnsafeRegex(pattern: string): boolean {
|
||||
// Detect nested quantifiers: a quantifier applied to a group that itself contains a quantifier
|
||||
// Examples: (a+)+, (a+)*, (.*)+, (\w+)*, (a|b+)+
|
||||
// This catches the most common ReDoS patterns
|
||||
const nestedQuantifier = /\([^)]*[+*][^)]*\)[+*{]/;
|
||||
if (nestedQuantifier.test(pattern)) return true;
|
||||
|
||||
// Detect overlapping alternations with quantifiers: (a|a)+, (\w|\d)+
|
||||
const overlappingAlternation = /\([^)]*\|[^)]*\)[+*{]/;
|
||||
// Only flag if alternation branches share characters (heuristic: both contain \w, ., or same literal)
|
||||
if (overlappingAlternation.test(pattern)) {
|
||||
const match = pattern.match(/\(([^)]*)\|([^)]*)\)[+*{]/);
|
||||
if (match) {
|
||||
const [, left, right] = match;
|
||||
// Flag if both branches use broad character classes
|
||||
const broadClasses = ['.', '\\w', '\\d', '\\s', '\\S', '\\W', '\\D', '[^'];
|
||||
const leftHasBroad = broadClasses.some(c => left.includes(c));
|
||||
const rightHasBroad = broadClasses.some(c => right.includes(c));
|
||||
if (leftHasBroad && rightHasBroad) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function countOccurrences(str: string, search: string): number {
|
||||
let count = 0;
|
||||
let pos = 0;
|
||||
while ((pos = str.indexOf(search, pos)) !== -1) {
|
||||
count++;
|
||||
pos += search.length;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Not safe for concurrent use — create a new instance per request.
|
||||
* Instance state is reset at the start of each applyDiff() call.
|
||||
@@ -79,7 +129,7 @@ export class WorkflowDiffEngine {
|
||||
const workflowCopy = JSON.parse(JSON.stringify(workflow));
|
||||
|
||||
// Group operations by type for two-pass processing
|
||||
const nodeOperationTypes = ['addNode', 'removeNode', 'updateNode', 'moveNode', 'enableNode', 'disableNode'];
|
||||
const nodeOperationTypes = ['addNode', 'removeNode', 'updateNode', 'patchNodeField', 'moveNode', 'enableNode', 'disableNode'];
|
||||
const nodeOperations: Array<{ operation: WorkflowDiffOperation; index: number }> = [];
|
||||
const otherOperations: Array<{ operation: WorkflowDiffOperation; index: number }> = [];
|
||||
|
||||
@@ -296,6 +346,8 @@ export class WorkflowDiffEngine {
|
||||
return this.validateRemoveNode(workflow, operation);
|
||||
case 'updateNode':
|
||||
return this.validateUpdateNode(workflow, operation);
|
||||
case 'patchNodeField':
|
||||
return this.validatePatchNodeField(workflow, operation as PatchNodeFieldOperation);
|
||||
case 'moveNode':
|
||||
return this.validateMoveNode(workflow, operation);
|
||||
case 'enableNode':
|
||||
@@ -341,6 +393,9 @@ export class WorkflowDiffEngine {
|
||||
case 'updateNode':
|
||||
this.applyUpdateNode(workflow, operation);
|
||||
break;
|
||||
case 'patchNodeField':
|
||||
this.applyPatchNodeField(workflow, operation as PatchNodeFieldOperation);
|
||||
break;
|
||||
case 'moveNode':
|
||||
this.applyMoveNode(workflow, operation);
|
||||
break;
|
||||
@@ -498,6 +553,77 @@ export class WorkflowDiffEngine {
|
||||
return null;
|
||||
}
|
||||
|
||||
private validatePatchNodeField(workflow: Workflow, operation: PatchNodeFieldOperation): string | null {
|
||||
if (!operation.nodeId && !operation.nodeName) {
|
||||
return `patchNodeField requires either "nodeId" or "nodeName"`;
|
||||
}
|
||||
|
||||
if (!operation.fieldPath || typeof operation.fieldPath !== 'string') {
|
||||
return `patchNodeField requires a "fieldPath" string (e.g., "parameters.jsCode")`;
|
||||
}
|
||||
|
||||
// Prototype pollution protection
|
||||
const pathSegments = operation.fieldPath.split('.');
|
||||
if (pathSegments.some(k => DANGEROUS_PATH_KEYS.has(k))) {
|
||||
return `patchNodeField: fieldPath "${operation.fieldPath}" contains a forbidden key (__proto__, constructor, or prototype)`;
|
||||
}
|
||||
|
||||
if (!Array.isArray(operation.patches) || operation.patches.length === 0) {
|
||||
return `patchNodeField requires a non-empty "patches" array of {find, replace} objects`;
|
||||
}
|
||||
|
||||
// Resource limit: max patches per operation
|
||||
if (operation.patches.length > PATCH_LIMITS.MAX_PATCHES) {
|
||||
return `patchNodeField: too many patches (${operation.patches.length}). Maximum is ${PATCH_LIMITS.MAX_PATCHES} per operation. Split into multiple operations if needed.`;
|
||||
}
|
||||
|
||||
for (let i = 0; i < operation.patches.length; i++) {
|
||||
const patch = operation.patches[i];
|
||||
if (!patch || typeof patch.find !== 'string' || typeof patch.replace !== 'string') {
|
||||
return `Invalid patch entry at index ${i}: each entry must have "find" (string) and "replace" (string)`;
|
||||
}
|
||||
if (patch.find.length === 0) {
|
||||
return `Invalid patch entry at index ${i}: "find" must not be empty`;
|
||||
}
|
||||
if (patch.regex) {
|
||||
// Resource limit: max regex pattern length
|
||||
if (patch.find.length > PATCH_LIMITS.MAX_REGEX_LENGTH) {
|
||||
return `Regex pattern at patch index ${i} is too long (${patch.find.length} chars). Maximum is ${PATCH_LIMITS.MAX_REGEX_LENGTH} characters.`;
|
||||
}
|
||||
try {
|
||||
new RegExp(patch.find);
|
||||
} catch (e) {
|
||||
return `Invalid regex pattern at patch index ${i}: ${e instanceof Error ? e.message : 'invalid regex'}`;
|
||||
}
|
||||
// ReDoS protection: reject patterns with nested quantifiers
|
||||
if (isUnsafeRegex(patch.find)) {
|
||||
return `Potentially unsafe regex pattern at patch index ${i}: nested quantifiers or overlapping alternations can cause excessive backtracking. Simplify the pattern or use literal matching (regex: false).`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
|
||||
if (!node) {
|
||||
return this.formatNodeNotFoundError(workflow, operation.nodeId || operation.nodeName || '', 'patchNodeField');
|
||||
}
|
||||
|
||||
const currentValue = this.getNestedProperty(node, operation.fieldPath);
|
||||
if (currentValue === undefined) {
|
||||
return `Cannot apply patchNodeField to "${operation.fieldPath}": property does not exist on node "${node.name}"`;
|
||||
}
|
||||
if (typeof currentValue !== 'string') {
|
||||
return `Cannot apply patchNodeField to "${operation.fieldPath}": current value is ${typeof currentValue}, expected string`;
|
||||
}
|
||||
|
||||
// Resource limit: cap field size for regex operations
|
||||
const hasRegex = operation.patches.some(p => p.regex);
|
||||
if (hasRegex && typeof currentValue === 'string' && currentValue.length > PATCH_LIMITS.MAX_FIELD_SIZE_REGEX) {
|
||||
return `Field "${operation.fieldPath}" is too large for regex operations (${Math.round(currentValue.length / 1024)}KB). Maximum is ${PATCH_LIMITS.MAX_FIELD_SIZE_REGEX / 1024}KB. Use literal matching (regex: false) for large fields.`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private validateMoveNode(workflow: Workflow, operation: MoveNodeOperation): string | null {
|
||||
const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
|
||||
if (!node) {
|
||||
@@ -775,10 +901,74 @@ export class WorkflowDiffEngine {
|
||||
Object.assign(node, sanitized);
|
||||
}
|
||||
|
||||
private applyPatchNodeField(workflow: Workflow, operation: PatchNodeFieldOperation): void {
|
||||
const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
|
||||
if (!node) return;
|
||||
|
||||
this.modifiedNodeIds.add(node.id);
|
||||
|
||||
let current = this.getNestedProperty(node, operation.fieldPath) as string;
|
||||
|
||||
for (let i = 0; i < operation.patches.length; i++) {
|
||||
const patch = operation.patches[i];
|
||||
|
||||
if (patch.regex) {
|
||||
const globalRegex = new RegExp(patch.find, 'g');
|
||||
const matches = current.match(globalRegex);
|
||||
|
||||
if (!matches || matches.length === 0) {
|
||||
throw new Error(
|
||||
`patchNodeField: regex pattern "${patch.find}" not found in "${operation.fieldPath}" (patch index ${i}). ` +
|
||||
`Use n8n_get_workflow to inspect the current value.`
|
||||
);
|
||||
}
|
||||
|
||||
if (matches.length > 1 && !patch.replaceAll) {
|
||||
throw new Error(
|
||||
`patchNodeField: regex pattern "${patch.find}" matches ${matches.length} times in "${operation.fieldPath}" (patch index ${i}). ` +
|
||||
`Set "replaceAll": true to replace all occurrences, or refine the pattern to match exactly once.`
|
||||
);
|
||||
}
|
||||
|
||||
const regex = patch.replaceAll ? globalRegex : new RegExp(patch.find);
|
||||
current = current.replace(regex, patch.replace);
|
||||
} else {
|
||||
const occurrences = countOccurrences(current, patch.find);
|
||||
|
||||
if (occurrences === 0) {
|
||||
throw new Error(
|
||||
`patchNodeField: "${patch.find.substring(0, 80)}" not found in "${operation.fieldPath}" (patch index ${i}). ` +
|
||||
`Ensure the find string exactly matches the current content (including whitespace and newlines). ` +
|
||||
`Use n8n_get_workflow to inspect the current value.`
|
||||
);
|
||||
}
|
||||
|
||||
if (occurrences > 1 && !patch.replaceAll) {
|
||||
throw new Error(
|
||||
`patchNodeField: "${patch.find.substring(0, 80)}" found ${occurrences} times in "${operation.fieldPath}" (patch index ${i}). ` +
|
||||
`Set "replaceAll": true to replace all occurrences, or use a more specific find string that matches exactly once.`
|
||||
);
|
||||
}
|
||||
|
||||
if (patch.replaceAll) {
|
||||
current = current.split(patch.find).join(patch.replace);
|
||||
} else {
|
||||
current = current.replace(patch.find, patch.replace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.setNestedProperty(node, operation.fieldPath, current);
|
||||
|
||||
// Sanitize node after updates
|
||||
const sanitized = sanitizeNode(node);
|
||||
Object.assign(node, sanitized);
|
||||
}
|
||||
|
||||
private applyMoveNode(workflow: Workflow, operation: MoveNodeOperation): void {
|
||||
const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
|
||||
if (!node) return;
|
||||
|
||||
|
||||
node.position = operation.position;
|
||||
}
|
||||
|
||||
@@ -1320,6 +1510,7 @@ export class WorkflowDiffEngine {
|
||||
const keys = path.split('.');
|
||||
let current = obj;
|
||||
for (const key of keys) {
|
||||
if (DANGEROUS_PATH_KEYS.has(key)) return undefined;
|
||||
if (current == null || typeof current !== 'object') return undefined;
|
||||
current = current[key];
|
||||
}
|
||||
@@ -1330,6 +1521,11 @@ export class WorkflowDiffEngine {
|
||||
const keys = path.split('.');
|
||||
let current = obj;
|
||||
|
||||
// Prototype pollution protection
|
||||
if (keys.some(k => DANGEROUS_PATH_KEYS.has(k))) {
|
||||
throw new Error(`Invalid property path: "${path}" contains a forbidden key`);
|
||||
}
|
||||
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const key = keys[i];
|
||||
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {
|
||||
|
||||
@@ -55,6 +55,19 @@ export interface DisableNodeOperation extends DiffOperation {
|
||||
nodeName?: string;
|
||||
}
|
||||
|
||||
export interface PatchNodeFieldOperation extends DiffOperation {
|
||||
type: 'patchNodeField';
|
||||
nodeId?: string;
|
||||
nodeName?: string;
|
||||
fieldPath: string; // Dot-notation path, e.g. "parameters.jsCode"
|
||||
patches: Array<{
|
||||
find: string;
|
||||
replace: string;
|
||||
replaceAll?: boolean; // Default: false. Replace all occurrences.
|
||||
regex?: boolean; // Default: false. Treat find as a regex pattern.
|
||||
}>;
|
||||
}
|
||||
|
||||
// Connection Operations
|
||||
export interface AddConnectionOperation extends DiffOperation {
|
||||
type: 'addConnection';
|
||||
@@ -153,6 +166,7 @@ export type WorkflowDiffOperation =
|
||||
| AddNodeOperation
|
||||
| RemoveNodeOperation
|
||||
| UpdateNodeOperation
|
||||
| PatchNodeFieldOperation
|
||||
| MoveNodeOperation
|
||||
| EnableNodeOperation
|
||||
| DisableNodeOperation
|
||||
@@ -208,10 +222,10 @@ export interface NodeReference {
|
||||
}
|
||||
|
||||
// Utility functions type guards
|
||||
export function isNodeOperation(op: WorkflowDiffOperation): op is
|
||||
AddNodeOperation | RemoveNodeOperation | UpdateNodeOperation |
|
||||
export function isNodeOperation(op: WorkflowDiffOperation): op is
|
||||
AddNodeOperation | RemoveNodeOperation | UpdateNodeOperation | PatchNodeFieldOperation |
|
||||
MoveNodeOperation | EnableNodeOperation | DisableNodeOperation {
|
||||
return ['addNode', 'removeNode', 'updateNode', 'moveNode', 'enableNode', 'disableNode'].includes(op.type);
|
||||
return ['addNode', 'removeNode', 'updateNode', 'patchNodeField', 'moveNode', 'enableNode', 'disableNode'].includes(op.type);
|
||||
}
|
||||
|
||||
export function isConnectionOperation(op: WorkflowDiffOperation): op is
|
||||
|
||||
@@ -428,6 +428,22 @@ describe('WorkflowDiffEngine', () => {
|
||||
expect(result.errors![0].message).toContain('Correct structure:');
|
||||
});
|
||||
|
||||
it('should reject prototype pollution via update path', async () => {
|
||||
const result = await diffEngine.applyDiff(baseWorkflow, {
|
||||
id: 'test',
|
||||
operations: [{
|
||||
type: 'updateNode' as const,
|
||||
nodeId: 'http-1',
|
||||
updates: {
|
||||
'__proto__.polluted': 'malicious'
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors?.[0]?.message).toContain('forbidden key');
|
||||
});
|
||||
|
||||
it('should apply __patch_find_replace to string properties (#642)', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(baseWorkflow));
|
||||
workflow.nodes.push({
|
||||
@@ -581,6 +597,520 @@ describe('WorkflowDiffEngine', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('PatchNodeField Operation', () => {
|
||||
it('should apply single find/replace patch', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(baseWorkflow));
|
||||
workflow.nodes.push({
|
||||
id: 'code-1',
|
||||
name: 'Code',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 1,
|
||||
position: [900, 300],
|
||||
parameters: { jsCode: 'const x = 1;\nreturn x + 2;' }
|
||||
});
|
||||
|
||||
const result = await diffEngine.applyDiff(workflow, {
|
||||
id: 'test',
|
||||
operations: [{
|
||||
type: 'patchNodeField' as const,
|
||||
nodeName: 'Code',
|
||||
fieldPath: 'parameters.jsCode',
|
||||
patches: [{ find: 'x + 2', replace: 'x + 3' }]
|
||||
}]
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const codeNode = result.workflow.nodes.find((n: any) => n.name === 'Code');
|
||||
expect(codeNode?.parameters.jsCode).toBe('const x = 1;\nreturn x + 3;');
|
||||
});
|
||||
|
||||
it('should error when find string not found', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(baseWorkflow));
|
||||
workflow.nodes.push({
|
||||
id: 'code-1',
|
||||
name: 'Code',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 1,
|
||||
position: [900, 300],
|
||||
parameters: { jsCode: 'const x = 1;' }
|
||||
});
|
||||
|
||||
const result = await diffEngine.applyDiff(workflow, {
|
||||
id: 'test',
|
||||
operations: [{
|
||||
type: 'patchNodeField' as const,
|
||||
nodeName: 'Code',
|
||||
fieldPath: 'parameters.jsCode',
|
||||
patches: [{ find: 'nonexistent text', replace: 'something' }]
|
||||
}]
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors?.[0]?.message).toContain('not found');
|
||||
});
|
||||
|
||||
it('should error on ambiguous match (multiple occurrences)', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(baseWorkflow));
|
||||
workflow.nodes.push({
|
||||
id: 'code-1',
|
||||
name: 'Code',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 1,
|
||||
position: [900, 300],
|
||||
parameters: { jsCode: 'const a = 1;\nconst b = 1;\nconst c = 1;' }
|
||||
});
|
||||
|
||||
const result = await diffEngine.applyDiff(workflow, {
|
||||
id: 'test',
|
||||
operations: [{
|
||||
type: 'patchNodeField' as const,
|
||||
nodeName: 'Code',
|
||||
fieldPath: 'parameters.jsCode',
|
||||
patches: [{ find: 'const', replace: 'let' }]
|
||||
}]
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors?.[0]?.message).toContain('3 times');
|
||||
expect(result.errors?.[0]?.message).toContain('replaceAll');
|
||||
});
|
||||
|
||||
it('should replace all occurrences with replaceAll flag', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(baseWorkflow));
|
||||
workflow.nodes.push({
|
||||
id: 'code-1',
|
||||
name: 'Code',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 1,
|
||||
position: [900, 300],
|
||||
parameters: { jsCode: 'const a = 1;\nconst b = 2;\nconst c = 3;' }
|
||||
});
|
||||
|
||||
const result = await diffEngine.applyDiff(workflow, {
|
||||
id: 'test',
|
||||
operations: [{
|
||||
type: 'patchNodeField' as const,
|
||||
nodeName: 'Code',
|
||||
fieldPath: 'parameters.jsCode',
|
||||
patches: [{ find: 'const', replace: 'let', replaceAll: true }]
|
||||
}]
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const codeNode = result.workflow.nodes.find((n: any) => n.name === 'Code');
|
||||
expect(codeNode?.parameters.jsCode).toBe('let a = 1;\nlet b = 2;\nlet c = 3;');
|
||||
});
|
||||
|
||||
it('should apply multiple sequential patches', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(baseWorkflow));
|
||||
workflow.nodes.push({
|
||||
id: 'code-1',
|
||||
name: 'Code',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 1,
|
||||
position: [900, 300],
|
||||
parameters: { jsCode: 'const a = 1;\nconst b = 2;\nreturn a + b;' }
|
||||
});
|
||||
|
||||
const result = await diffEngine.applyDiff(workflow, {
|
||||
id: 'test',
|
||||
operations: [{
|
||||
type: 'patchNodeField' as const,
|
||||
nodeName: 'Code',
|
||||
fieldPath: 'parameters.jsCode',
|
||||
patches: [
|
||||
{ find: 'const a = 1', replace: 'const a = 10' },
|
||||
{ find: 'const b = 2', replace: 'const b = 20' }
|
||||
]
|
||||
}]
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const codeNode = result.workflow.nodes.find((n: any) => n.name === 'Code');
|
||||
expect(codeNode?.parameters.jsCode).toBe('const a = 10;\nconst b = 20;\nreturn a + b;');
|
||||
});
|
||||
|
||||
it('should support regex pattern matching', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(baseWorkflow));
|
||||
workflow.nodes.push({
|
||||
id: 'code-1',
|
||||
name: 'Code',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 1,
|
||||
position: [900, 300],
|
||||
parameters: { jsCode: 'const limit = 42;' }
|
||||
});
|
||||
|
||||
const result = await diffEngine.applyDiff(workflow, {
|
||||
id: 'test',
|
||||
operations: [{
|
||||
type: 'patchNodeField' as const,
|
||||
nodeName: 'Code',
|
||||
fieldPath: 'parameters.jsCode',
|
||||
patches: [{ find: 'const limit = \\d+', replace: 'const limit = 100', regex: true }]
|
||||
}]
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const codeNode = result.workflow.nodes.find((n: any) => n.name === 'Code');
|
||||
expect(codeNode?.parameters.jsCode).toBe('const limit = 100;');
|
||||
});
|
||||
|
||||
it('should support regex with replaceAll', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(baseWorkflow));
|
||||
workflow.nodes.push({
|
||||
id: 'code-1',
|
||||
name: 'Code',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 1,
|
||||
position: [900, 300],
|
||||
parameters: { jsCode: 'item1 = 10;\nitem2 = 20;\nitem3 = 30;' }
|
||||
});
|
||||
|
||||
const result = await diffEngine.applyDiff(workflow, {
|
||||
id: 'test',
|
||||
operations: [{
|
||||
type: 'patchNodeField' as const,
|
||||
nodeName: 'Code',
|
||||
fieldPath: 'parameters.jsCode',
|
||||
patches: [{ find: 'item\\d+', replace: 'val', regex: true, replaceAll: true }]
|
||||
}]
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const codeNode = result.workflow.nodes.find((n: any) => n.name === 'Code');
|
||||
expect(codeNode?.parameters.jsCode).toBe('val = 10;\nval = 20;\nval = 30;');
|
||||
});
|
||||
|
||||
it('should error on ambiguous regex match without replaceAll', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(baseWorkflow));
|
||||
workflow.nodes.push({
|
||||
id: 'code-1',
|
||||
name: 'Code',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 1,
|
||||
position: [900, 300],
|
||||
parameters: { jsCode: 'item1 = 10;\nitem2 = 20;' }
|
||||
});
|
||||
|
||||
const result = await diffEngine.applyDiff(workflow, {
|
||||
id: 'test',
|
||||
operations: [{
|
||||
type: 'patchNodeField' as const,
|
||||
nodeName: 'Code',
|
||||
fieldPath: 'parameters.jsCode',
|
||||
patches: [{ find: 'item\\d+', replace: 'val', regex: true }]
|
||||
}]
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors?.[0]?.message).toContain('2 times');
|
||||
});
|
||||
|
||||
it('should reject invalid regex pattern in validation', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(baseWorkflow));
|
||||
workflow.nodes.push({
|
||||
id: 'code-1',
|
||||
name: 'Code',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 1,
|
||||
position: [900, 300],
|
||||
parameters: { jsCode: 'const x = 1;' }
|
||||
});
|
||||
|
||||
const result = await diffEngine.applyDiff(workflow, {
|
||||
id: 'test',
|
||||
operations: [{
|
||||
type: 'patchNodeField' as const,
|
||||
nodeName: 'Code',
|
||||
fieldPath: 'parameters.jsCode',
|
||||
patches: [{ find: '(unclosed', replace: 'x', regex: true }]
|
||||
}]
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors?.[0]?.message).toContain('Invalid regex');
|
||||
});
|
||||
|
||||
it('should error on non-existent field', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(baseWorkflow));
|
||||
workflow.nodes.push({
|
||||
id: 'code-1',
|
||||
name: 'Code',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 1,
|
||||
position: [900, 300],
|
||||
parameters: { jsCode: 'const x = 1;' }
|
||||
});
|
||||
|
||||
const result = await diffEngine.applyDiff(workflow, {
|
||||
id: 'test',
|
||||
operations: [{
|
||||
type: 'patchNodeField' as const,
|
||||
nodeName: 'Code',
|
||||
fieldPath: 'parameters.nonExistent',
|
||||
patches: [{ find: 'x', replace: 'y' }]
|
||||
}]
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors?.[0]?.message).toContain('does not exist');
|
||||
});
|
||||
|
||||
it('should error on non-string field', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(baseWorkflow));
|
||||
workflow.nodes.push({
|
||||
id: 'code-1',
|
||||
name: 'Code',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 1,
|
||||
position: [900, 300],
|
||||
parameters: { retryCount: 3 }
|
||||
});
|
||||
|
||||
const result = await diffEngine.applyDiff(workflow, {
|
||||
id: 'test',
|
||||
operations: [{
|
||||
type: 'patchNodeField' as const,
|
||||
nodeName: 'Code',
|
||||
fieldPath: 'parameters.retryCount',
|
||||
patches: [{ find: '3', replace: '5' }]
|
||||
}]
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors?.[0]?.message).toContain('expected string');
|
||||
});
|
||||
|
||||
it('should error on missing node', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(baseWorkflow));
|
||||
|
||||
const result = await diffEngine.applyDiff(workflow, {
|
||||
id: 'test',
|
||||
operations: [{
|
||||
type: 'patchNodeField' as const,
|
||||
nodeName: 'NonExistent',
|
||||
fieldPath: 'parameters.jsCode',
|
||||
patches: [{ find: 'x', replace: 'y' }]
|
||||
}]
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors?.[0]?.message).toContain('not found');
|
||||
});
|
||||
|
||||
it('should reject empty patches array', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(baseWorkflow));
|
||||
workflow.nodes.push({
|
||||
id: 'code-1',
|
||||
name: 'Code',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 1,
|
||||
position: [900, 300],
|
||||
parameters: { jsCode: 'const x = 1;' }
|
||||
});
|
||||
|
||||
const result = await diffEngine.applyDiff(workflow, {
|
||||
id: 'test',
|
||||
operations: [{
|
||||
type: 'patchNodeField' as const,
|
||||
nodeName: 'Code',
|
||||
fieldPath: 'parameters.jsCode',
|
||||
patches: []
|
||||
}]
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors?.[0]?.message).toContain('non-empty');
|
||||
});
|
||||
|
||||
it('should reject empty find string', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(baseWorkflow));
|
||||
workflow.nodes.push({
|
||||
id: 'code-1',
|
||||
name: 'Code',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 1,
|
||||
position: [900, 300],
|
||||
parameters: { jsCode: 'const x = 1;' }
|
||||
});
|
||||
|
||||
const result = await diffEngine.applyDiff(workflow, {
|
||||
id: 'test',
|
||||
operations: [{
|
||||
type: 'patchNodeField' as const,
|
||||
nodeName: 'Code',
|
||||
fieldPath: 'parameters.jsCode',
|
||||
patches: [{ find: '', replace: 'y' }]
|
||||
}]
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors?.[0]?.message).toContain('must not be empty');
|
||||
});
|
||||
|
||||
it('should work with nested fieldPath using dot notation', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(baseWorkflow));
|
||||
workflow.nodes.push({
|
||||
id: 'set-1',
|
||||
name: 'Set',
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 3,
|
||||
position: [900, 300],
|
||||
parameters: {
|
||||
options: {
|
||||
template: '<p>Hello World</p>'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const result = await diffEngine.applyDiff(workflow, {
|
||||
id: 'test',
|
||||
operations: [{
|
||||
type: 'patchNodeField' as const,
|
||||
nodeName: 'Set',
|
||||
fieldPath: 'parameters.options.template',
|
||||
patches: [{ find: 'Hello World', replace: 'Goodbye World' }]
|
||||
}]
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const setNode = result.workflow.nodes.find((n: any) => n.name === 'Set');
|
||||
expect(setNode?.parameters.options.template).toBe('<p>Goodbye World</p>');
|
||||
});
|
||||
|
||||
it('should reject prototype pollution via fieldPath', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(baseWorkflow));
|
||||
workflow.nodes.push({
|
||||
id: 'code-1',
|
||||
name: 'Code',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 1,
|
||||
position: [900, 300],
|
||||
parameters: { jsCode: 'const x = 1;' }
|
||||
});
|
||||
|
||||
const result = await diffEngine.applyDiff(workflow, {
|
||||
id: 'test',
|
||||
operations: [{
|
||||
type: 'patchNodeField' as const,
|
||||
nodeName: 'Code',
|
||||
fieldPath: '__proto__.polluted',
|
||||
patches: [{ find: 'x', replace: 'y' }]
|
||||
}]
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors?.[0]?.message).toContain('forbidden key');
|
||||
});
|
||||
|
||||
it('should reject unsafe regex patterns (ReDoS)', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(baseWorkflow));
|
||||
workflow.nodes.push({
|
||||
id: 'code-1',
|
||||
name: 'Code',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 1,
|
||||
position: [900, 300],
|
||||
parameters: { jsCode: 'const x = 1;' }
|
||||
});
|
||||
|
||||
const result = await diffEngine.applyDiff(workflow, {
|
||||
id: 'test',
|
||||
operations: [{
|
||||
type: 'patchNodeField' as const,
|
||||
nodeName: 'Code',
|
||||
fieldPath: 'parameters.jsCode',
|
||||
patches: [{ find: '(a+)+$', replace: 'safe', regex: true }]
|
||||
}]
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors?.[0]?.message).toContain('unsafe regex');
|
||||
});
|
||||
|
||||
it('should reject too many patches', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(baseWorkflow));
|
||||
workflow.nodes.push({
|
||||
id: 'code-1',
|
||||
name: 'Code',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 1,
|
||||
position: [900, 300],
|
||||
parameters: { jsCode: 'const x = 1;' }
|
||||
});
|
||||
|
||||
const patches = Array.from({ length: 51 }, (_, i) => ({
|
||||
find: `pattern${i}`,
|
||||
replace: `replacement${i}`
|
||||
}));
|
||||
|
||||
const result = await diffEngine.applyDiff(workflow, {
|
||||
id: 'test',
|
||||
operations: [{
|
||||
type: 'patchNodeField' as const,
|
||||
nodeName: 'Code',
|
||||
fieldPath: 'parameters.jsCode',
|
||||
patches
|
||||
}]
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors?.[0]?.message).toContain('too many patches');
|
||||
});
|
||||
|
||||
it('should reject overly long regex patterns', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(baseWorkflow));
|
||||
workflow.nodes.push({
|
||||
id: 'code-1',
|
||||
name: 'Code',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 1,
|
||||
position: [900, 300],
|
||||
parameters: { jsCode: 'const x = 1;' }
|
||||
});
|
||||
|
||||
const result = await diffEngine.applyDiff(workflow, {
|
||||
id: 'test',
|
||||
operations: [{
|
||||
type: 'patchNodeField' as const,
|
||||
nodeName: 'Code',
|
||||
fieldPath: 'parameters.jsCode',
|
||||
patches: [{ find: 'a'.repeat(501), replace: 'b', regex: true }]
|
||||
}]
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors?.[0]?.message).toContain('too long');
|
||||
});
|
||||
|
||||
it('should work with nodeId reference', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(baseWorkflow));
|
||||
workflow.nodes.push({
|
||||
id: 'code-1',
|
||||
name: 'Code',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 1,
|
||||
position: [900, 300],
|
||||
parameters: { jsCode: 'const x = 1;' }
|
||||
});
|
||||
|
||||
const result = await diffEngine.applyDiff(workflow, {
|
||||
id: 'test',
|
||||
operations: [{
|
||||
type: 'patchNodeField' as const,
|
||||
nodeId: 'code-1',
|
||||
fieldPath: 'parameters.jsCode',
|
||||
patches: [{ find: 'const x = 1', replace: 'const x = 2' }]
|
||||
}]
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const codeNode = result.workflow.nodes.find((n: any) => n.id === 'code-1');
|
||||
expect(codeNode?.parameters.jsCode).toBe('const x = 2;');
|
||||
});
|
||||
});
|
||||
|
||||
describe('MoveNode Operation', () => {
|
||||
it('should move node to new position', async () => {
|
||||
const operation: MoveNodeOperation = {
|
||||
|
||||
Reference in New Issue
Block a user