mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-03-21 09:53:08 +00:00
Add transferWorkflow diff operation to move workflows between n8n projects:
- TransferWorkflowOperation type with destinationProjectId field
- WorkflowDiffEngine validates and tracks transfer intent
- Handler calls PUT /workflows/{id}/transfer after update
- N8nApiClient.transferWorkflow() method
- Zod schema validates destinationProjectId is non-empty
- Tool description and documentation updated
- inferIntentFromOperations case for transfer
Also fixes two pre-existing bugs found during review:
- continueOnError path now properly extracts/propagates activation flags
- Debug log in updateConnectionReferences shows correct old name
Based on work by @djakielski in PR #645.
Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
This commit is contained in:
committed by
GitHub
parent
14962a39b6
commit
47a1cb135d
@@ -17,7 +17,8 @@ import {
|
||||
AddTagOperation,
|
||||
RemoveTagOperation,
|
||||
CleanStaleConnectionsOperation,
|
||||
ReplaceConnectionsOperation
|
||||
ReplaceConnectionsOperation,
|
||||
TransferWorkflowOperation
|
||||
} from '@/types/workflow-diff';
|
||||
import { Workflow } from '@/types/n8n-api';
|
||||
|
||||
@@ -4989,4 +4990,151 @@ describe('WorkflowDiffEngine', () => {
|
||||
expect('nonExistent' in updatedNode).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('transferWorkflow operation', () => {
|
||||
it('should set transferToProjectId in result for valid transferWorkflow', async () => {
|
||||
const operation: TransferWorkflowOperation = {
|
||||
type: 'transferWorkflow',
|
||||
destinationProjectId: 'project-abc-123'
|
||||
};
|
||||
|
||||
const request: WorkflowDiffRequest = {
|
||||
id: 'test-workflow',
|
||||
operations: [operation]
|
||||
};
|
||||
|
||||
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.transferToProjectId).toBe('project-abc-123');
|
||||
});
|
||||
|
||||
it('should fail validation when destinationProjectId is empty', async () => {
|
||||
const operation: TransferWorkflowOperation = {
|
||||
type: 'transferWorkflow',
|
||||
destinationProjectId: ''
|
||||
};
|
||||
|
||||
const request: WorkflowDiffRequest = {
|
||||
id: 'test-workflow',
|
||||
operations: [operation]
|
||||
};
|
||||
|
||||
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toBeDefined();
|
||||
expect(result.errors![0].message).toContain('destinationProjectId');
|
||||
});
|
||||
|
||||
it('should fail validation when destinationProjectId is undefined', async () => {
|
||||
const operation = {
|
||||
type: 'transferWorkflow',
|
||||
destinationProjectId: undefined
|
||||
} as any as TransferWorkflowOperation;
|
||||
|
||||
const request: WorkflowDiffRequest = {
|
||||
id: 'test-workflow',
|
||||
operations: [operation]
|
||||
};
|
||||
|
||||
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toBeDefined();
|
||||
expect(result.errors![0].message).toContain('destinationProjectId');
|
||||
});
|
||||
|
||||
it('should not include transferToProjectId when no transferWorkflow operation is present', async () => {
|
||||
const operation: UpdateNameOperation = {
|
||||
type: 'updateName',
|
||||
name: 'Renamed Workflow'
|
||||
};
|
||||
|
||||
const request: WorkflowDiffRequest = {
|
||||
id: 'test-workflow',
|
||||
operations: [operation]
|
||||
};
|
||||
|
||||
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.transferToProjectId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should combine updateName and transferWorkflow operations', async () => {
|
||||
const operations: WorkflowDiffOperation[] = [
|
||||
{
|
||||
type: 'updateName',
|
||||
name: 'Transferred Workflow'
|
||||
} as UpdateNameOperation,
|
||||
{
|
||||
type: 'transferWorkflow',
|
||||
destinationProjectId: 'project-xyz-789'
|
||||
} as TransferWorkflowOperation
|
||||
];
|
||||
|
||||
const request: WorkflowDiffRequest = {
|
||||
id: 'test-workflow',
|
||||
operations
|
||||
};
|
||||
|
||||
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.operationsApplied).toBe(2);
|
||||
expect(result.workflow!.name).toBe('Transferred Workflow');
|
||||
expect(result.transferToProjectId).toBe('project-xyz-789');
|
||||
});
|
||||
|
||||
it('should combine removeTag and transferWorkflow in continueOnError mode', async () => {
|
||||
const operations: WorkflowDiffOperation[] = [
|
||||
{
|
||||
type: 'removeTag',
|
||||
tag: 'non-existent-tag'
|
||||
} as RemoveTagOperation,
|
||||
{
|
||||
type: 'transferWorkflow',
|
||||
destinationProjectId: 'project-target-456'
|
||||
} as TransferWorkflowOperation
|
||||
];
|
||||
|
||||
const request: WorkflowDiffRequest = {
|
||||
id: 'test-workflow',
|
||||
operations,
|
||||
continueOnError: true
|
||||
};
|
||||
|
||||
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.transferToProjectId).toBe('project-target-456');
|
||||
});
|
||||
|
||||
it('should fail entire batch in atomic mode when transferWorkflow has empty destinationProjectId alongside updateName', async () => {
|
||||
const operations: WorkflowDiffOperation[] = [
|
||||
{
|
||||
type: 'updateName',
|
||||
name: 'Should Not Apply'
|
||||
} as UpdateNameOperation,
|
||||
{
|
||||
type: 'transferWorkflow',
|
||||
destinationProjectId: ''
|
||||
} as TransferWorkflowOperation
|
||||
];
|
||||
|
||||
const request: WorkflowDiffRequest = {
|
||||
id: 'test-workflow',
|
||||
operations
|
||||
};
|
||||
|
||||
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toBeDefined();
|
||||
expect(result.errors![0].message).toContain('destinationProjectId');
|
||||
// In atomic mode, the workflow should not be returned since the batch failed
|
||||
expect(result.workflow).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user