mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-03-21 18:03:07 +00:00
Compare commits
3 Commits
v2.37.4
...
feat/creat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e2da6c652 | ||
|
|
4a9e3c7ec0 | ||
|
|
47a1cb135d |
36
CHANGELOG.md
36
CHANGELOG.md
@@ -7,6 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [2.40.0] - 2026-03-21
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **`n8n_manage_datatable` MCP tool** (replaces `n8n_create_data_table`): Full data table management covering all 10 n8n data table API endpoints
|
||||||
|
- **Table operations**: createTable, listTables, getTable, updateTable, deleteTable
|
||||||
|
- **Row operations**: getRows, insertRows, updateRows, upsertRows, deleteRows
|
||||||
|
- Filter system with and/or logic and 8 condition operators (eq, neq, like, ilike, gt, gte, lt, lte)
|
||||||
|
- Dry-run support for updateRows, upsertRows, deleteRows
|
||||||
|
- Pagination, sorting, and full-text search for row listing
|
||||||
|
- Shared error handler and consolidated Zod schemas for consistency
|
||||||
|
- 9 new `N8nApiClient` methods for all data table endpoints
|
||||||
|
- **`projectId` parameter for `n8n_create_workflow`**: Create workflows directly in a specific team project (enterprise feature)
|
||||||
|
|
||||||
|
### Breaking
|
||||||
|
|
||||||
|
- `n8n_create_data_table` tool replaced by `n8n_manage_datatable` with `action: "createTable"`
|
||||||
|
|
||||||
|
Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
|
||||||
|
|
||||||
|
## [2.38.0] - 2026-03-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **`transferWorkflow` diff operation** (Issue #644): Move workflows between projects via `n8n_update_partial_workflow`
|
||||||
|
- New `transferWorkflow` operation type with `destinationProjectId` parameter
|
||||||
|
- Calls `PUT /workflows/{id}/transfer` via dedicated API after workflow update
|
||||||
|
- Proper error handling: returns `{ success: false, saved: true }` when transfer fails after update
|
||||||
|
- Transfer executes before activation so workflow is in target project first
|
||||||
|
- Zod schema validates `destinationProjectId` is non-empty
|
||||||
|
- Updated tool description and documentation to list the new operation
|
||||||
|
- `inferIntentFromOperations` returns descriptive intent for transfer operations
|
||||||
|
- `N8nApiClient.transferWorkflow()` method added
|
||||||
|
|
||||||
|
Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
|
||||||
|
|
||||||
## [2.37.4] - 2026-03-18
|
## [2.37.4] - 2026-03-18
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
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;AAgF7D,wBAAsB,2BAA2B,CAC/C,IAAI,EAAE,OAAO,EACb,UAAU,EAAE,cAAc,EAC1B,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,eAAe,CAAC,CA6Z1B"}
|
{"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"}
|
||||||
24
dist/mcp/handlers-workflow-diff.js
vendored
24
dist/mcp/handlers-workflow-diff.js
vendored
@@ -79,6 +79,7 @@ const workflowDiffSchema = zod_1.z.object({
|
|||||||
settings: zod_1.z.any().optional(),
|
settings: zod_1.z.any().optional(),
|
||||||
name: zod_1.z.string().optional(),
|
name: zod_1.z.string().optional(),
|
||||||
tag: zod_1.z.string().optional(),
|
tag: zod_1.z.string().optional(),
|
||||||
|
destinationProjectId: zod_1.z.string().min(1).optional(),
|
||||||
id: zod_1.z.string().optional(),
|
id: zod_1.z.string().optional(),
|
||||||
}).transform((op) => {
|
}).transform((op) => {
|
||||||
if (NODE_TARGETING_OPERATIONS.has(op.type)) {
|
if (NODE_TARGETING_OPERATIONS.has(op.type)) {
|
||||||
@@ -329,6 +330,25 @@ async function handleUpdatePartialWorkflow(args, repository, context) {
|
|||||||
logger_1.logger.warn('Tag operations failed (non-blocking)', tagError);
|
logger_1.logger.warn('Tag operations failed (non-blocking)', tagError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let transferMessage = '';
|
||||||
|
if (diffResult.transferToProjectId) {
|
||||||
|
try {
|
||||||
|
await client.transferWorkflow(input.id, diffResult.transferToProjectId);
|
||||||
|
transferMessage = ` Workflow transferred to project ${diffResult.transferToProjectId}.`;
|
||||||
|
}
|
||||||
|
catch (transferError) {
|
||||||
|
logger_1.logger.error('Failed to transfer workflow to project', transferError);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
saved: true,
|
||||||
|
error: 'Workflow updated successfully but project transfer failed',
|
||||||
|
details: {
|
||||||
|
workflowUpdated: true,
|
||||||
|
transferError: transferError instanceof Error ? transferError.message : 'Unknown error'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
let finalWorkflow = updatedWorkflow;
|
let finalWorkflow = updatedWorkflow;
|
||||||
let activationMessage = '';
|
let activationMessage = '';
|
||||||
try {
|
try {
|
||||||
@@ -409,7 +429,7 @@ async function handleUpdatePartialWorkflow(args, repository, context) {
|
|||||||
nodeCount: finalWorkflow.nodes?.length || 0,
|
nodeCount: finalWorkflow.nodes?.length || 0,
|
||||||
operationsApplied: diffResult.operationsApplied
|
operationsApplied: diffResult.operationsApplied
|
||||||
},
|
},
|
||||||
message: `Workflow "${finalWorkflow.name}" updated successfully. Applied ${diffResult.operationsApplied} operations.${activationMessage} Use n8n_get_workflow with mode 'structure' to verify current state.`,
|
message: `Workflow "${finalWorkflow.name}" updated successfully. Applied ${diffResult.operationsApplied} operations.${transferMessage}${activationMessage} Use n8n_get_workflow with mode 'structure' to verify current state.`,
|
||||||
details: {
|
details: {
|
||||||
applied: diffResult.applied,
|
applied: diffResult.applied,
|
||||||
failed: diffResult.failed,
|
failed: diffResult.failed,
|
||||||
@@ -498,6 +518,8 @@ function inferIntentFromOperations(operations) {
|
|||||||
return 'Activate workflow';
|
return 'Activate workflow';
|
||||||
case 'deactivateWorkflow':
|
case 'deactivateWorkflow':
|
||||||
return 'Deactivate workflow';
|
return 'Deactivate workflow';
|
||||||
|
case 'transferWorkflow':
|
||||||
|
return `Transfer workflow to project ${op.destinationProjectId || ''}`.trim();
|
||||||
default:
|
default:
|
||||||
return `Workflow ${op.type}`;
|
return `Workflow ${op.type}`;
|
||||||
}
|
}
|
||||||
|
|||||||
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,iBA+ZzC,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,iBAuazC,CAAC"}
|
||||||
@@ -5,7 +5,7 @@ exports.n8nUpdatePartialWorkflowDoc = {
|
|||||||
name: 'n8n_update_partial_workflow',
|
name: 'n8n_update_partial_workflow',
|
||||||
category: 'workflow_management',
|
category: 'workflow_management',
|
||||||
essentials: {
|
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. 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, 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'],
|
keyParameters: ['id', 'operations', 'continueOnError'],
|
||||||
example: 'n8n_update_partial_workflow({id: "wf_123", operations: [{type: "rewireConnection", source: "IF", from: "Old", to: "New", branch: "true"}]})',
|
example: 'n8n_update_partial_workflow({id: "wf_123", operations: [{type: "rewireConnection", source: "IF", from: "Old", to: "New", branch: "true"}]})',
|
||||||
performance: 'Fast (50-200ms)',
|
performance: 'Fast (50-200ms)',
|
||||||
@@ -23,7 +23,8 @@ exports.n8nUpdatePartialWorkflowDoc = {
|
|||||||
'Batch AI component connections for atomic updates',
|
'Batch AI component connections for atomic updates',
|
||||||
'Auto-sanitization: ALL nodes auto-fixed during updates (operator structures, missing metadata)',
|
'Auto-sanitization: ALL nodes auto-fixed during updates (operator structures, missing metadata)',
|
||||||
'Node renames automatically update all connection references - no manual connection operations needed',
|
'Node renames automatically update all connection references - no manual connection operations needed',
|
||||||
'Activate/deactivate workflows: Use activateWorkflow/deactivateWorkflow operations (requires activatable triggers like webhook/schedule)'
|
'Activate/deactivate workflows: Use activateWorkflow/deactivateWorkflow operations (requires activatable triggers like webhook/schedule)',
|
||||||
|
'Transfer workflows between projects: Use transferWorkflow with destinationProjectId (enterprise feature)'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
full: {
|
full: {
|
||||||
@@ -56,6 +57,9 @@ exports.n8nUpdatePartialWorkflowDoc = {
|
|||||||
- **activateWorkflow**: Activate the workflow to enable automatic execution via triggers
|
- **activateWorkflow**: Activate the workflow to enable automatic execution via triggers
|
||||||
- **deactivateWorkflow**: Deactivate the workflow to prevent automatic execution
|
- **deactivateWorkflow**: Deactivate the workflow to prevent automatic execution
|
||||||
|
|
||||||
|
### Project Management Operations (1 type):
|
||||||
|
- **transferWorkflow**: Transfer the workflow to a different project. Requires \`destinationProjectId\`. Enterprise/cloud feature.
|
||||||
|
|
||||||
## Smart Parameters for Multi-Output Nodes
|
## Smart Parameters for Multi-Output Nodes
|
||||||
|
|
||||||
For **IF nodes**, use semantic 'branch' parameter instead of technical sourceIndex:
|
For **IF nodes**, use semantic 'branch' parameter instead of technical sourceIndex:
|
||||||
@@ -346,7 +350,10 @@ n8n_update_partial_workflow({
|
|||||||
'// Migrate from deprecated continueOnFail to onError\nn8n_update_partial_workflow({id: "rm2", operations: [{type: "updateNode", nodeName: "HTTP Request", updates: {continueOnFail: null, onError: "continueErrorOutput"}}]})',
|
'// Migrate from deprecated continueOnFail to onError\nn8n_update_partial_workflow({id: "rm2", operations: [{type: "updateNode", nodeName: "HTTP Request", updates: {continueOnFail: null, onError: "continueErrorOutput"}}]})',
|
||||||
'// Remove nested property\nn8n_update_partial_workflow({id: "rm3", operations: [{type: "updateNode", nodeName: "API Request", updates: {"parameters.authentication": null}}]})',
|
'// Remove nested property\nn8n_update_partial_workflow({id: "rm3", operations: [{type: "updateNode", nodeName: "API Request", updates: {"parameters.authentication": null}}]})',
|
||||||
'// Remove multiple properties\nn8n_update_partial_workflow({id: "rm4", operations: [{type: "updateNode", nodeName: "Data Processor", updates: {continueOnFail: null, alwaysOutputData: null, "parameters.legacy_option": null}}]})',
|
'// Remove multiple properties\nn8n_update_partial_workflow({id: "rm4", operations: [{type: "updateNode", nodeName: "Data Processor", updates: {continueOnFail: null, alwaysOutputData: null, "parameters.legacy_option": null}}]})',
|
||||||
'// Remove entire array property\nn8n_update_partial_workflow({id: "rm5", operations: [{type: "updateNode", nodeName: "HTTP Request", updates: {"parameters.headers": null}}]})'
|
'// Remove entire array property\nn8n_update_partial_workflow({id: "rm5", operations: [{type: "updateNode", nodeName: "HTTP Request", updates: {"parameters.headers": null}}]})',
|
||||||
|
'\n// ============ PROJECT TRANSFER EXAMPLES ============',
|
||||||
|
'// Transfer workflow to a different project\nn8n_update_partial_workflow({id: "tf1", operations: [{type: "transferWorkflow", destinationProjectId: "project-abc-123"}]})',
|
||||||
|
'// Transfer and activate in one call\nn8n_update_partial_workflow({id: "tf2", operations: [{type: "transferWorkflow", destinationProjectId: "project-abc-123"}, {type: "activateWorkflow"}]})'
|
||||||
],
|
],
|
||||||
useCases: [
|
useCases: [
|
||||||
'Rewire connections when replacing nodes',
|
'Rewire connections when replacing nodes',
|
||||||
@@ -364,7 +371,8 @@ n8n_update_partial_workflow({
|
|||||||
'Add fallback language models to AI Agents',
|
'Add fallback language models to AI Agents',
|
||||||
'Configure Vector Store retrieval systems',
|
'Configure Vector Store retrieval systems',
|
||||||
'Swap language models in existing AI workflows',
|
'Swap language models in existing AI workflows',
|
||||||
'Batch-update AI tool connections'
|
'Batch-update AI tool connections',
|
||||||
|
'Transfer workflows between team projects (enterprise)'
|
||||||
],
|
],
|
||||||
performance: 'Very fast - typically 50-200ms. Much faster than full updates as only changes are processed.',
|
performance: 'Very fast - typically 50-200ms. Much faster than full updates as only changes are processed.',
|
||||||
bestPractices: [
|
bestPractices: [
|
||||||
|
|||||||
@@ -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
@@ -137,7 +137,7 @@ exports.n8nManagementTools = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'n8n_update_partial_workflow',
|
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. See tools_documentation("n8n_update_partial_workflow", "full") for details.`,
|
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.`,
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
additionalProperties: true,
|
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
1
dist/services/n8n-api-client.d.ts
vendored
1
dist/services/n8n-api-client.d.ts
vendored
@@ -20,6 +20,7 @@ export declare class N8nApiClient {
|
|||||||
getWorkflow(id: string): Promise<Workflow>;
|
getWorkflow(id: string): Promise<Workflow>;
|
||||||
updateWorkflow(id: string, workflow: Partial<Workflow>): Promise<Workflow>;
|
updateWorkflow(id: string, workflow: Partial<Workflow>): Promise<Workflow>;
|
||||||
deleteWorkflow(id: string): Promise<Workflow>;
|
deleteWorkflow(id: string): Promise<Workflow>;
|
||||||
|
transferWorkflow(id: string, destinationProjectId: string): Promise<void>;
|
||||||
activateWorkflow(id: string): Promise<Workflow>;
|
activateWorkflow(id: string): Promise<Workflow>;
|
||||||
deactivateWorkflow(id: string): Promise<Workflow>;
|
deactivateWorkflow(id: string): Promise<Workflow>;
|
||||||
listWorkflows(params?: WorkflowListParams): Promise<WorkflowListResponse>;
|
listWorkflows(params?: WorkflowListParams): Promise<WorkflowListResponse>;
|
||||||
|
|||||||
2
dist/services/n8n-api-client.d.ts.map
vendored
2
dist/services/n8n-api-client.d.ts.map
vendored
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"n8n-api-client.d.ts","sourceRoot":"","sources":["../../src/services/n8n-api-client.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,QAAQ,EACR,kBAAkB,EAClB,oBAAoB,EACpB,SAAS,EACT,mBAAmB,EACnB,qBAAqB,EACrB,UAAU,EACV,oBAAoB,EACpB,sBAAsB,EACtB,GAAG,EACH,aAAa,EACb,eAAe,EACf,mBAAmB,EACnB,cAAc,EACd,QAAQ,EACR,cAAc,EAGd,mBAAmB,EACnB,uBAAuB,EACvB,uBAAuB,EACxB,MAAM,kBAAkB,CAAC;AAS1B,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,WAAW,CAA+B;IAClD,OAAO,CAAC,cAAc,CAA+C;gBAEzD,MAAM,EAAE,kBAAkB;IAqDhC,UAAU,IAAI,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;YAyBpC,gBAAgB;IAa9B,oBAAoB,IAAI,cAAc,GAAG,IAAI;IAKvC,WAAW,IAAI,OAAO,CAAC,mBAAmB,CAAC;IA6C3C,cAAc,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC;IAU9D,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAS1C,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC;IAsC1E,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAS7C,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAS/C,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAsBjD,aAAa,CAAC,MAAM,GAAE,kBAAuB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAU7E,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,WAAW,UAAQ,GAAG,OAAO,CAAC,SAAS,CAAC;IAwBjE,cAAc,CAAC,MAAM,GAAE,mBAAwB,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAShF,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAS1C,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,GAAG,CAAC;IAiErD,eAAe,CAAC,MAAM,GAAE,oBAAyB,GAAG,OAAO,CAAC,sBAAsB,CAAC;IASnF,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAS9C,gBAAgB,CAAC,UAAU,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC;IAStE,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC;IASlF,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAsB3C,QAAQ,CAAC,MAAM,GAAE,aAAkB,GAAG,OAAO,CAAC,eAAe,CAAC;IAS9D,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC;IAS1C,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC;IAStD,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQpC,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAUxE,sBAAsB,IAAI,OAAO,CAAC,mBAAmB,CAAC;IAStD,iBAAiB,CAAC,KAAK,UAAQ,GAAG,OAAO,CAAC,uBAAuB,CAAC;IASlE,iBAAiB,CACrB,OAAO,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,MAAM,EAAE,GACnB,OAAO,CAAC,uBAAuB,CAAC;IAa7B,YAAY,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;IAWnC,cAAc,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC;IAS9D,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC;IAS1E,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiB/C,OAAO,CAAC,oBAAoB;CAmC7B"}
|
{"version":3,"file":"n8n-api-client.d.ts","sourceRoot":"","sources":["../../src/services/n8n-api-client.ts"],"names":[],"mappings":"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"}
|
||||||
8
dist/services/n8n-api-client.js
vendored
8
dist/services/n8n-api-client.js
vendored
@@ -194,6 +194,14 @@ class N8nApiClient {
|
|||||||
throw (0, n8n_errors_1.handleN8nApiError)(error);
|
throw (0, n8n_errors_1.handleN8nApiError)(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async transferWorkflow(id, destinationProjectId) {
|
||||||
|
try {
|
||||||
|
await this.client.put(`/workflows/${id}/transfer`, { destinationProjectId });
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
throw (0, n8n_errors_1.handleN8nApiError)(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
async activateWorkflow(id) {
|
async activateWorkflow(id) {
|
||||||
try {
|
try {
|
||||||
const response = await this.client.post(`/workflows/${id}/activate`, {});
|
const response = await this.client.post(`/workflows/${id}/activate`, {});
|
||||||
|
|||||||
2
dist/services/n8n-api-client.js.map
vendored
2
dist/services/n8n-api-client.js.map
vendored
File diff suppressed because one or more lines are too long
3
dist/services/workflow-diff-engine.d.ts
vendored
3
dist/services/workflow-diff-engine.d.ts
vendored
@@ -7,6 +7,7 @@ export declare class WorkflowDiffEngine {
|
|||||||
private removedNodeNames;
|
private removedNodeNames;
|
||||||
private tagsToAdd;
|
private tagsToAdd;
|
||||||
private tagsToRemove;
|
private tagsToRemove;
|
||||||
|
private transferToProjectId;
|
||||||
applyDiff(workflow: Workflow, request: WorkflowDiffRequest): Promise<WorkflowDiffResult>;
|
applyDiff(workflow: Workflow, request: WorkflowDiffRequest): Promise<WorkflowDiffResult>;
|
||||||
private validateOperation;
|
private validateOperation;
|
||||||
private applyOperation;
|
private applyOperation;
|
||||||
@@ -36,6 +37,8 @@ export declare class WorkflowDiffEngine {
|
|||||||
private validateDeactivateWorkflow;
|
private validateDeactivateWorkflow;
|
||||||
private applyActivateWorkflow;
|
private applyActivateWorkflow;
|
||||||
private applyDeactivateWorkflow;
|
private applyDeactivateWorkflow;
|
||||||
|
private validateTransferWorkflow;
|
||||||
|
private applyTransferWorkflow;
|
||||||
private validateCleanStaleConnections;
|
private validateCleanStaleConnections;
|
||||||
private validateReplaceConnections;
|
private validateReplaceConnections;
|
||||||
private applyCleanStaleConnections;
|
private applyCleanStaleConnections;
|
||||||
|
|||||||
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,EAsBnB,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;IAK9B,SAAS,CACb,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,kBAAkB,CAAC;IAqN9B,OAAO,CAAC,iBAAiB;IAwCzB,OAAO,CAAC,cAAc;IAyDtB,OAAO,CAAC,eAAe;IAwBvB,OAAO,CAAC,kBAAkB;IAuB1B,OAAO,CAAC,kBAAkB;IAoC1B,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;IA0BvB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,gBAAgB;IAWxB,OAAO,CAAC,sBAAsB;IAwD9B,OAAO,CAAC,kBAAkB;IA6C1B,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,6BAA6B;IAKrC,OAAO,CAAC,0BAA0B;IA0BlC,OAAO,CAAC,0BAA0B;IA+ElC,OAAO,CAAC,uBAAuB;IAe/B,OAAO,CAAC,0BAA0B;IAkElC,OAAO,CAAC,iBAAiB;IAkBzB,OAAO,CAAC,QAAQ;IAsChB,OAAO,CAAC,uBAAuB;IAW/B,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,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;IAoC1B,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;IA0BvB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,gBAAgB;IAWxB,OAAO,CAAC,sBAAsB;IAwD9B,OAAO,CAAC,kBAAkB;IA6C1B,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;CAoB1B"}
|
||||||
30
dist/services/workflow-diff-engine.js
vendored
30
dist/services/workflow-diff-engine.js
vendored
@@ -23,6 +23,7 @@ class WorkflowDiffEngine {
|
|||||||
this.removedNodeNames.clear();
|
this.removedNodeNames.clear();
|
||||||
this.tagsToAdd = [];
|
this.tagsToAdd = [];
|
||||||
this.tagsToRemove = [];
|
this.tagsToRemove = [];
|
||||||
|
this.transferToProjectId = undefined;
|
||||||
const workflowCopy = JSON.parse(JSON.stringify(workflow));
|
const workflowCopy = JSON.parse(JSON.stringify(workflow));
|
||||||
const nodeOperationTypes = ['addNode', 'removeNode', 'updateNode', 'moveNode', 'enableNode', 'disableNode'];
|
const nodeOperationTypes = ['addNode', 'removeNode', 'updateNode', 'moveNode', 'enableNode', 'disableNode'];
|
||||||
const nodeOperations = [];
|
const nodeOperations = [];
|
||||||
@@ -81,6 +82,10 @@ class WorkflowDiffEngine {
|
|||||||
failed: failedIndices
|
failed: failedIndices
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const shouldActivate = workflowCopy._shouldActivate === true;
|
||||||
|
const shouldDeactivate = workflowCopy._shouldDeactivate === true;
|
||||||
|
delete workflowCopy._shouldActivate;
|
||||||
|
delete workflowCopy._shouldDeactivate;
|
||||||
const success = appliedIndices.length > 0;
|
const success = appliedIndices.length > 0;
|
||||||
return {
|
return {
|
||||||
success,
|
success,
|
||||||
@@ -91,8 +96,11 @@ class WorkflowDiffEngine {
|
|||||||
warnings: this.warnings.length > 0 ? this.warnings : undefined,
|
warnings: this.warnings.length > 0 ? this.warnings : undefined,
|
||||||
applied: appliedIndices,
|
applied: appliedIndices,
|
||||||
failed: failedIndices,
|
failed: failedIndices,
|
||||||
|
shouldActivate: shouldActivate || undefined,
|
||||||
|
shouldDeactivate: shouldDeactivate || undefined,
|
||||||
tagsToAdd: this.tagsToAdd.length > 0 ? this.tagsToAdd : undefined,
|
tagsToAdd: this.tagsToAdd.length > 0 ? this.tagsToAdd : undefined,
|
||||||
tagsToRemove: this.tagsToRemove.length > 0 ? this.tagsToRemove : undefined
|
tagsToRemove: this.tagsToRemove.length > 0 ? this.tagsToRemove : undefined,
|
||||||
|
transferToProjectId: this.transferToProjectId || undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -181,7 +189,8 @@ class WorkflowDiffEngine {
|
|||||||
shouldActivate: shouldActivate || undefined,
|
shouldActivate: shouldActivate || undefined,
|
||||||
shouldDeactivate: shouldDeactivate || undefined,
|
shouldDeactivate: shouldDeactivate || undefined,
|
||||||
tagsToAdd: this.tagsToAdd.length > 0 ? this.tagsToAdd : undefined,
|
tagsToAdd: this.tagsToAdd.length > 0 ? this.tagsToAdd : undefined,
|
||||||
tagsToRemove: this.tagsToRemove.length > 0 ? this.tagsToRemove : undefined
|
tagsToRemove: this.tagsToRemove.length > 0 ? this.tagsToRemove : undefined,
|
||||||
|
transferToProjectId: this.transferToProjectId || undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -220,6 +229,8 @@ class WorkflowDiffEngine {
|
|||||||
case 'addTag':
|
case 'addTag':
|
||||||
case 'removeTag':
|
case 'removeTag':
|
||||||
return null;
|
return null;
|
||||||
|
case 'transferWorkflow':
|
||||||
|
return this.validateTransferWorkflow(workflow, operation);
|
||||||
case 'activateWorkflow':
|
case 'activateWorkflow':
|
||||||
return this.validateActivateWorkflow(workflow, operation);
|
return this.validateActivateWorkflow(workflow, operation);
|
||||||
case 'deactivateWorkflow':
|
case 'deactivateWorkflow':
|
||||||
@@ -285,6 +296,9 @@ class WorkflowDiffEngine {
|
|||||||
case 'replaceConnections':
|
case 'replaceConnections':
|
||||||
this.applyReplaceConnections(workflow, operation);
|
this.applyReplaceConnections(workflow, operation);
|
||||||
break;
|
break;
|
||||||
|
case 'transferWorkflow':
|
||||||
|
this.applyTransferWorkflow(workflow, operation);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
validateAddNode(workflow, operation) {
|
validateAddNode(workflow, operation) {
|
||||||
@@ -699,6 +713,15 @@ class WorkflowDiffEngine {
|
|||||||
applyDeactivateWorkflow(workflow, operation) {
|
applyDeactivateWorkflow(workflow, operation) {
|
||||||
workflow._shouldDeactivate = true;
|
workflow._shouldDeactivate = true;
|
||||||
}
|
}
|
||||||
|
validateTransferWorkflow(_workflow, operation) {
|
||||||
|
if (!operation.destinationProjectId) {
|
||||||
|
return 'transferWorkflow requires a non-empty destinationProjectId string';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
applyTransferWorkflow(_workflow, operation) {
|
||||||
|
this.transferToProjectId = operation.destinationProjectId;
|
||||||
|
}
|
||||||
validateCleanStaleConnections(workflow, operation) {
|
validateCleanStaleConnections(workflow, operation) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -806,9 +829,10 @@ class WorkflowDiffEngine {
|
|||||||
for (let connIndex = 0; connIndex < connectionsAtIndex.length; connIndex++) {
|
for (let connIndex = 0; connIndex < connectionsAtIndex.length; connIndex++) {
|
||||||
const connection = connectionsAtIndex[connIndex];
|
const connection = connectionsAtIndex[connIndex];
|
||||||
if (renames.has(connection.node)) {
|
if (renames.has(connection.node)) {
|
||||||
|
const oldTargetName = connection.node;
|
||||||
const newTargetName = renames.get(connection.node);
|
const newTargetName = renames.get(connection.node);
|
||||||
connection.node = newTargetName;
|
connection.node = newTargetName;
|
||||||
logger.debug(`Updated connection: ${sourceName}[${outputType}][${outputIndex}][${connIndex}].node: "${connection.node}" → "${newTargetName}"`);
|
logger.debug(`Updated connection: ${sourceName}[${outputType}][${outputIndex}][${connIndex}].node: "${oldTargetName}" → "${newTargetName}"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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
7
dist/types/workflow-diff.d.ts
vendored
7
dist/types/workflow-diff.d.ts
vendored
@@ -94,6 +94,10 @@ export interface ActivateWorkflowOperation extends DiffOperation {
|
|||||||
export interface DeactivateWorkflowOperation extends DiffOperation {
|
export interface DeactivateWorkflowOperation extends DiffOperation {
|
||||||
type: 'deactivateWorkflow';
|
type: 'deactivateWorkflow';
|
||||||
}
|
}
|
||||||
|
export interface TransferWorkflowOperation extends DiffOperation {
|
||||||
|
type: 'transferWorkflow';
|
||||||
|
destinationProjectId: string;
|
||||||
|
}
|
||||||
export interface CleanStaleConnectionsOperation extends DiffOperation {
|
export interface CleanStaleConnectionsOperation extends DiffOperation {
|
||||||
type: 'cleanStaleConnections';
|
type: 'cleanStaleConnections';
|
||||||
dryRun?: boolean;
|
dryRun?: boolean;
|
||||||
@@ -110,7 +114,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;
|
export type WorkflowDiffOperation = AddNodeOperation | RemoveNodeOperation | UpdateNodeOperation | MoveNodeOperation | EnableNodeOperation | DisableNodeOperation | AddConnectionOperation | RemoveConnectionOperation | RewireConnectionOperation | UpdateSettingsOperation | UpdateNameOperation | AddTagOperation | RemoveTagOperation | ActivateWorkflowOperation | DeactivateWorkflowOperation | CleanStaleConnectionsOperation | ReplaceConnectionsOperation | TransferWorkflowOperation;
|
||||||
export interface WorkflowDiffRequest {
|
export interface WorkflowDiffRequest {
|
||||||
id: string;
|
id: string;
|
||||||
operations: WorkflowDiffOperation[];
|
operations: WorkflowDiffOperation[];
|
||||||
@@ -139,6 +143,7 @@ export interface WorkflowDiffResult {
|
|||||||
shouldDeactivate?: boolean;
|
shouldDeactivate?: boolean;
|
||||||
tagsToAdd?: string[];
|
tagsToAdd?: string[];
|
||||||
tagsToRemove?: string[];
|
tagsToRemove?: string[];
|
||||||
|
transferToProjectId?: string;
|
||||||
}
|
}
|
||||||
export interface NodeReference {
|
export interface NodeReference {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|||||||
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.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":";;AA2MA,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":";;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"}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "n8n-mcp",
|
"name": "n8n-mcp",
|
||||||
"version": "2.37.4",
|
"version": "2.40.0",
|
||||||
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
|
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
WebhookRequest,
|
WebhookRequest,
|
||||||
McpToolResponse,
|
McpToolResponse,
|
||||||
ExecutionFilterOptions,
|
ExecutionFilterOptions,
|
||||||
ExecutionMode
|
ExecutionMode,
|
||||||
} from '../types/n8n-api';
|
} from '../types/n8n-api';
|
||||||
import type { TriggerType, TestWorkflowInput } from '../triggers/types';
|
import type { TriggerType, TestWorkflowInput } from '../triggers/types';
|
||||||
import {
|
import {
|
||||||
@@ -383,6 +383,7 @@ const createWorkflowSchema = z.object({
|
|||||||
executionTimeout: z.number().optional(),
|
executionTimeout: z.number().optional(),
|
||||||
errorWorkflow: z.string().optional(),
|
errorWorkflow: z.string().optional(),
|
||||||
}).optional(),
|
}).optional(),
|
||||||
|
projectId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateWorkflowSchema = z.object({
|
const updateWorkflowSchema = z.object({
|
||||||
@@ -1974,7 +1975,7 @@ export async function handleDiagnostic(request: any, context?: InstanceContext):
|
|||||||
|
|
||||||
// Check which tools are available
|
// Check which tools are available
|
||||||
const documentationTools = 7; // Base documentation tools (after v2.26.0 consolidation)
|
const documentationTools = 7; // Base documentation tools (after v2.26.0 consolidation)
|
||||||
const managementTools = apiConfigured ? 13 : 0; // Management tools requiring API (includes n8n_deploy_template)
|
const managementTools = apiConfigured ? 14 : 0; // Management tools requiring API (includes n8n_manage_datatable)
|
||||||
const totalTools = documentationTools + managementTools;
|
const totalTools = documentationTools + managementTools;
|
||||||
|
|
||||||
// Check npm version
|
// Check npm version
|
||||||
@@ -2688,3 +2689,243 @@ export async function handleTriggerWebhookWorkflow(args: unknown, context?: Inst
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Data Table Handlers
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
// Shared Zod schemas for data table operations
|
||||||
|
const dataTableFilterConditionSchema = z.object({
|
||||||
|
columnName: z.string().min(1),
|
||||||
|
condition: z.enum(['eq', 'neq', 'like', 'ilike', 'gt', 'gte', 'lt', 'lte']),
|
||||||
|
value: z.any(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataTableFilterSchema = z.object({
|
||||||
|
type: z.enum(['and', 'or']).optional().default('and'),
|
||||||
|
filters: z.array(dataTableFilterConditionSchema).min(1, 'At least one filter condition is required'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Shared base schema for actions requiring a tableId
|
||||||
|
const tableIdSchema = z.object({
|
||||||
|
tableId: z.string().min(1, 'tableId is required'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Per-action Zod schemas
|
||||||
|
const createTableSchema = z.object({
|
||||||
|
name: z.string().min(1, 'Table name cannot be empty'),
|
||||||
|
columns: z.array(z.object({
|
||||||
|
name: z.string().min(1, 'Column name cannot be empty'),
|
||||||
|
type: z.enum(['string', 'number', 'boolean', 'date', 'json']).optional(),
|
||||||
|
})).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const listTablesSchema = z.object({
|
||||||
|
limit: z.number().min(1).max(100).optional(),
|
||||||
|
cursor: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateTableSchema = tableIdSchema.extend({
|
||||||
|
name: z.string().min(1, 'New table name cannot be empty'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getRowsSchema = tableIdSchema.extend({
|
||||||
|
limit: z.number().min(1).max(100).optional(),
|
||||||
|
cursor: z.string().optional(),
|
||||||
|
filter: z.union([dataTableFilterSchema, z.string()]).optional(),
|
||||||
|
sortBy: z.string().optional(),
|
||||||
|
search: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const insertRowsSchema = tableIdSchema.extend({
|
||||||
|
data: z.array(z.record(z.unknown())).min(1, 'At least one row is required'),
|
||||||
|
returnType: z.enum(['count', 'id', 'all']).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Shared schema for update/upsert (identical structure)
|
||||||
|
const mutateRowsSchema = tableIdSchema.extend({
|
||||||
|
filter: dataTableFilterSchema,
|
||||||
|
data: z.record(z.unknown()),
|
||||||
|
returnData: z.boolean().optional(),
|
||||||
|
dryRun: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteRowsSchema = tableIdSchema.extend({
|
||||||
|
filter: dataTableFilterSchema,
|
||||||
|
returnData: z.boolean().optional(),
|
||||||
|
dryRun: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleDataTableError(error: unknown): McpToolResponse {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
return { success: false, error: 'Invalid input', details: { errors: error.errors } };
|
||||||
|
}
|
||||||
|
if (error instanceof N8nApiError) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: getUserFriendlyErrorMessage(error),
|
||||||
|
code: error.code,
|
||||||
|
details: error.details as Record<string, unknown> | undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { success: false, error: error instanceof Error ? error.message : 'Unknown error occurred' };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleCreateTable(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
|
||||||
|
try {
|
||||||
|
const client = ensureApiConfigured(context);
|
||||||
|
const input = createTableSchema.parse(args);
|
||||||
|
const dataTable = await client.createDataTable(input);
|
||||||
|
if (!dataTable || !dataTable.id) {
|
||||||
|
return { success: false, error: 'Data table creation failed: n8n API returned an empty or invalid response' };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: { id: dataTable.id, name: dataTable.name },
|
||||||
|
message: `Data table "${dataTable.name}" created with ID: ${dataTable.id}`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return handleDataTableError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleListTables(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
|
||||||
|
try {
|
||||||
|
const client = ensureApiConfigured(context);
|
||||||
|
const input = listTablesSchema.parse(args || {});
|
||||||
|
const result = await client.listDataTables(input);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
tables: result.data,
|
||||||
|
count: result.data.length,
|
||||||
|
nextCursor: result.nextCursor || undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return handleDataTableError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleGetTable(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
|
||||||
|
try {
|
||||||
|
const client = ensureApiConfigured(context);
|
||||||
|
const { tableId } = tableIdSchema.parse(args);
|
||||||
|
const dataTable = await client.getDataTable(tableId);
|
||||||
|
return { success: true, data: dataTable };
|
||||||
|
} catch (error) {
|
||||||
|
return handleDataTableError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleUpdateTable(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
|
||||||
|
try {
|
||||||
|
const client = ensureApiConfigured(context);
|
||||||
|
const { tableId, name } = updateTableSchema.parse(args);
|
||||||
|
const dataTable = await client.updateDataTable(tableId, { name });
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: dataTable,
|
||||||
|
message: `Data table renamed to "${dataTable.name}"`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return handleDataTableError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleDeleteTable(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
|
||||||
|
try {
|
||||||
|
const client = ensureApiConfigured(context);
|
||||||
|
const { tableId } = tableIdSchema.parse(args);
|
||||||
|
await client.deleteDataTable(tableId);
|
||||||
|
return { success: true, message: `Data table ${tableId} deleted successfully` };
|
||||||
|
} catch (error) {
|
||||||
|
return handleDataTableError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleGetRows(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
|
||||||
|
try {
|
||||||
|
const client = ensureApiConfigured(context);
|
||||||
|
const { tableId, filter, ...params } = getRowsSchema.parse(args);
|
||||||
|
const queryParams: Record<string, unknown> = { ...params };
|
||||||
|
if (filter) {
|
||||||
|
queryParams.filter = typeof filter === 'string' ? filter : JSON.stringify(filter);
|
||||||
|
}
|
||||||
|
const result = await client.getDataTableRows(tableId, queryParams as any);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
rows: result.data,
|
||||||
|
count: result.data.length,
|
||||||
|
nextCursor: result.nextCursor || undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return handleDataTableError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleInsertRows(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
|
||||||
|
try {
|
||||||
|
const client = ensureApiConfigured(context);
|
||||||
|
const { tableId, ...params } = insertRowsSchema.parse(args);
|
||||||
|
const result = await client.insertDataTableRows(tableId, params);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: result,
|
||||||
|
message: `Rows inserted into data table ${tableId}`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return handleDataTableError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleUpdateRows(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
|
||||||
|
try {
|
||||||
|
const client = ensureApiConfigured(context);
|
||||||
|
const { tableId, ...params } = mutateRowsSchema.parse(args);
|
||||||
|
const result = await client.updateDataTableRows(tableId, params);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: result,
|
||||||
|
message: params.dryRun ? 'Dry run: rows matched (no changes applied)' : 'Rows updated successfully',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return handleDataTableError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleUpsertRows(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
|
||||||
|
try {
|
||||||
|
const client = ensureApiConfigured(context);
|
||||||
|
const { tableId, ...params } = mutateRowsSchema.parse(args);
|
||||||
|
const result = await client.upsertDataTableRow(tableId, params);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: result,
|
||||||
|
message: params.dryRun ? 'Dry run: upsert previewed (no changes applied)' : 'Row upserted successfully',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return handleDataTableError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleDeleteRows(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
|
||||||
|
try {
|
||||||
|
const client = ensureApiConfigured(context);
|
||||||
|
const { tableId, filter, ...params } = deleteRowsSchema.parse(args);
|
||||||
|
const queryParams = {
|
||||||
|
filter: JSON.stringify(filter),
|
||||||
|
...params,
|
||||||
|
};
|
||||||
|
const result = await client.deleteDataTableRows(tableId, queryParams as any);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: result,
|
||||||
|
message: params.dryRun ? 'Dry run: rows matched for deletion (no changes applied)' : 'Rows deleted successfully',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return handleDataTableError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ const workflowDiffSchema = z.object({
|
|||||||
settings: z.any().optional(),
|
settings: z.any().optional(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
tag: z.string().optional(),
|
tag: z.string().optional(),
|
||||||
|
// Transfer operation
|
||||||
|
destinationProjectId: z.string().min(1).optional(),
|
||||||
// Aliases: LLMs often use "id" instead of "nodeId" — accept both
|
// Aliases: LLMs often use "id" instead of "nodeId" — accept both
|
||||||
id: z.string().optional(),
|
id: z.string().optional(),
|
||||||
}).transform((op) => {
|
}).transform((op) => {
|
||||||
@@ -370,6 +372,26 @@ export async function handleUpdatePartialWorkflow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle project transfer if requested (before activation so workflow is in target project first)
|
||||||
|
let transferMessage = '';
|
||||||
|
if (diffResult.transferToProjectId) {
|
||||||
|
try {
|
||||||
|
await client.transferWorkflow(input.id, diffResult.transferToProjectId);
|
||||||
|
transferMessage = ` Workflow transferred to project ${diffResult.transferToProjectId}.`;
|
||||||
|
} catch (transferError) {
|
||||||
|
logger.error('Failed to transfer workflow to project', transferError);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
saved: true,
|
||||||
|
error: 'Workflow updated successfully but project transfer failed',
|
||||||
|
details: {
|
||||||
|
workflowUpdated: true,
|
||||||
|
transferError: transferError instanceof Error ? transferError.message : 'Unknown error'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle activation/deactivation if requested
|
// Handle activation/deactivation if requested
|
||||||
let finalWorkflow = updatedWorkflow;
|
let finalWorkflow = updatedWorkflow;
|
||||||
let activationMessage = '';
|
let activationMessage = '';
|
||||||
@@ -454,7 +476,7 @@ export async function handleUpdatePartialWorkflow(
|
|||||||
nodeCount: finalWorkflow.nodes?.length || 0,
|
nodeCount: finalWorkflow.nodes?.length || 0,
|
||||||
operationsApplied: diffResult.operationsApplied
|
operationsApplied: diffResult.operationsApplied
|
||||||
},
|
},
|
||||||
message: `Workflow "${finalWorkflow.name}" updated successfully. Applied ${diffResult.operationsApplied} operations.${activationMessage} Use n8n_get_workflow with mode 'structure' to verify current state.`,
|
message: `Workflow "${finalWorkflow.name}" updated successfully. Applied ${diffResult.operationsApplied} operations.${transferMessage}${activationMessage} Use n8n_get_workflow with mode 'structure' to verify current state.`,
|
||||||
details: {
|
details: {
|
||||||
applied: diffResult.applied,
|
applied: diffResult.applied,
|
||||||
failed: diffResult.failed,
|
failed: diffResult.failed,
|
||||||
@@ -559,6 +581,8 @@ function inferIntentFromOperations(operations: any[]): string {
|
|||||||
return 'Activate workflow';
|
return 'Activate workflow';
|
||||||
case 'deactivateWorkflow':
|
case 'deactivateWorkflow':
|
||||||
return 'Deactivate workflow';
|
return 'Deactivate workflow';
|
||||||
|
case 'transferWorkflow':
|
||||||
|
return `Transfer workflow to project ${op.destinationProjectId || ''}`.trim();
|
||||||
default:
|
default:
|
||||||
return `Workflow ${op.type}`;
|
return `Workflow ${op.type}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1029,6 +1029,11 @@ export class N8NDocumentationMCPServer {
|
|||||||
? { valid: true, errors: [] }
|
? { valid: true, errors: [] }
|
||||||
: { valid: false, errors: [{ field: 'action', message: 'action is required' }] };
|
: { valid: false, errors: [{ field: 'action', message: 'action is required' }] };
|
||||||
break;
|
break;
|
||||||
|
case 'n8n_manage_datatable':
|
||||||
|
validationResult = args.action
|
||||||
|
? { valid: true, errors: [] }
|
||||||
|
: { valid: false, errors: [{ field: 'action', message: 'action is required' }] };
|
||||||
|
break;
|
||||||
case 'n8n_deploy_template':
|
case 'n8n_deploy_template':
|
||||||
// Requires templateId parameter
|
// Requires templateId parameter
|
||||||
validationResult = args.templateId !== undefined
|
validationResult = args.templateId !== undefined
|
||||||
@@ -1496,6 +1501,26 @@ export class N8NDocumentationMCPServer {
|
|||||||
if (!this.repository) throw new Error('Repository not initialized');
|
if (!this.repository) throw new Error('Repository not initialized');
|
||||||
return n8nHandlers.handleDeployTemplate(args, this.templateService, this.repository, this.instanceContext);
|
return n8nHandlers.handleDeployTemplate(args, this.templateService, this.repository, this.instanceContext);
|
||||||
|
|
||||||
|
case 'n8n_manage_datatable': {
|
||||||
|
this.validateToolParams(name, args, ['action']);
|
||||||
|
const dtAction = args.action;
|
||||||
|
// Each handler validates its own inputs via Zod schemas
|
||||||
|
switch (dtAction) {
|
||||||
|
case 'createTable': return n8nHandlers.handleCreateTable(args, this.instanceContext);
|
||||||
|
case 'listTables': return n8nHandlers.handleListTables(args, this.instanceContext);
|
||||||
|
case 'getTable': return n8nHandlers.handleGetTable(args, this.instanceContext);
|
||||||
|
case 'updateTable': return n8nHandlers.handleUpdateTable(args, this.instanceContext);
|
||||||
|
case 'deleteTable': return n8nHandlers.handleDeleteTable(args, this.instanceContext);
|
||||||
|
case 'getRows': return n8nHandlers.handleGetRows(args, this.instanceContext);
|
||||||
|
case 'insertRows': return n8nHandlers.handleInsertRows(args, this.instanceContext);
|
||||||
|
case 'updateRows': return n8nHandlers.handleUpdateRows(args, this.instanceContext);
|
||||||
|
case 'upsertRows': return n8nHandlers.handleUpsertRows(args, this.instanceContext);
|
||||||
|
case 'deleteRows': return n8nHandlers.handleDeleteRows(args, this.instanceContext);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown action: ${dtAction}. Valid actions: createTable, listTables, getTable, updateTable, deleteTable, getRows, insertRows, updateRows, upsertRows, deleteRows`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown tool: ${name}`);
|
throw new Error(`Unknown tool: ${name}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ import {
|
|||||||
n8nTestWorkflowDoc,
|
n8nTestWorkflowDoc,
|
||||||
n8nExecutionsDoc,
|
n8nExecutionsDoc,
|
||||||
n8nWorkflowVersionsDoc,
|
n8nWorkflowVersionsDoc,
|
||||||
n8nDeployTemplateDoc
|
n8nDeployTemplateDoc,
|
||||||
|
n8nManageDatatableDoc
|
||||||
} from './workflow_management';
|
} from './workflow_management';
|
||||||
|
|
||||||
// Combine all tool documentations into a single object
|
// Combine all tool documentations into a single object
|
||||||
@@ -60,7 +61,8 @@ export const toolsDocumentation: Record<string, ToolDocumentation> = {
|
|||||||
n8n_test_workflow: n8nTestWorkflowDoc,
|
n8n_test_workflow: n8nTestWorkflowDoc,
|
||||||
n8n_executions: n8nExecutionsDoc,
|
n8n_executions: n8nExecutionsDoc,
|
||||||
n8n_workflow_versions: n8nWorkflowVersionsDoc,
|
n8n_workflow_versions: n8nWorkflowVersionsDoc,
|
||||||
n8n_deploy_template: n8nDeployTemplateDoc
|
n8n_deploy_template: n8nDeployTemplateDoc,
|
||||||
|
n8n_manage_datatable: n8nManageDatatableDoc
|
||||||
};
|
};
|
||||||
|
|
||||||
// Re-export types
|
// Re-export types
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ export { n8nTestWorkflowDoc } from './n8n-test-workflow';
|
|||||||
export { n8nExecutionsDoc } from './n8n-executions';
|
export { n8nExecutionsDoc } from './n8n-executions';
|
||||||
export { n8nWorkflowVersionsDoc } from './n8n-workflow-versions';
|
export { n8nWorkflowVersionsDoc } from './n8n-workflow-versions';
|
||||||
export { n8nDeployTemplateDoc } from './n8n-deploy-template';
|
export { n8nDeployTemplateDoc } from './n8n-deploy-template';
|
||||||
|
export { n8nManageDatatableDoc } from './n8n-manage-datatable';
|
||||||
|
|||||||
109
src/mcp/tool-docs/workflow_management/n8n-manage-datatable.ts
Normal file
109
src/mcp/tool-docs/workflow_management/n8n-manage-datatable.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { ToolDocumentation } from '../types';
|
||||||
|
|
||||||
|
export const n8nManageDatatableDoc: ToolDocumentation = {
|
||||||
|
name: 'n8n_manage_datatable',
|
||||||
|
category: 'workflow_management',
|
||||||
|
essentials: {
|
||||||
|
description: 'Manage n8n data tables and rows. Unified tool for table CRUD and row operations with filtering, pagination, and dry-run support.',
|
||||||
|
keyParameters: ['action', 'tableId', 'name', 'data', 'filter'],
|
||||||
|
example: 'n8n_manage_datatable({action: "createTable", name: "Contacts", columns: [{name: "email", type: "string"}]})',
|
||||||
|
performance: 'Fast (100-500ms)',
|
||||||
|
tips: [
|
||||||
|
'Table actions: createTable, listTables, getTable, updateTable, deleteTable',
|
||||||
|
'Row actions: getRows, insertRows, updateRows, upsertRows, deleteRows',
|
||||||
|
'Use dryRun: true to preview update/upsert/delete before applying',
|
||||||
|
'Filter supports: eq, neq, like, ilike, gt, gte, lt, lte conditions',
|
||||||
|
'Use returnData: true to get affected rows back from update/upsert/delete',
|
||||||
|
'Requires n8n enterprise or cloud with data tables feature'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
full: {
|
||||||
|
description: `**Table Actions:**
|
||||||
|
- **createTable**: Create a new data table with optional typed columns
|
||||||
|
- **listTables**: List all data tables (paginated)
|
||||||
|
- **getTable**: Get table details and column definitions by ID
|
||||||
|
- **updateTable**: Rename an existing table
|
||||||
|
- **deleteTable**: Permanently delete a table and all its rows
|
||||||
|
|
||||||
|
**Row Actions:**
|
||||||
|
- **getRows**: List rows with filtering, sorting, search, and pagination
|
||||||
|
- **insertRows**: Insert one or more rows (bulk)
|
||||||
|
- **updateRows**: Update rows matching a filter condition
|
||||||
|
- **upsertRows**: Update matching row or insert if none match
|
||||||
|
- **deleteRows**: Delete rows matching a filter condition (filter required)
|
||||||
|
|
||||||
|
**Filter System:** Used in getRows, updateRows, upsertRows, deleteRows
|
||||||
|
- Combine conditions with "and" (default) or "or"
|
||||||
|
- Conditions: eq, neq, like, ilike, gt, gte, lt, lte
|
||||||
|
- Example: {type: "and", filters: [{columnName: "status", condition: "eq", value: "active"}]}
|
||||||
|
|
||||||
|
**Dry Run:** updateRows, upsertRows, and deleteRows support dryRun: true to preview changes without applying them.`,
|
||||||
|
parameters: {
|
||||||
|
action: { type: 'string', required: true, description: 'Operation to perform' },
|
||||||
|
tableId: { type: 'string', required: false, description: 'Data table ID (required for all except createTable and listTables)' },
|
||||||
|
name: { type: 'string', required: false, description: 'For createTable/updateTable: table name' },
|
||||||
|
columns: { type: 'array', required: false, description: 'For createTable: column definitions [{name, type?}]. Types: string, number, boolean, date, json' },
|
||||||
|
data: { type: 'array|object', required: false, description: 'For insertRows: array of row objects. For updateRows/upsertRows: object with column values' },
|
||||||
|
filter: { type: 'object', required: false, description: 'Filter: {type?: "and"|"or", filters: [{columnName, condition, value}]}' },
|
||||||
|
limit: { type: 'number', required: false, description: 'For listTables/getRows: max results (1-100)' },
|
||||||
|
cursor: { type: 'string', required: false, description: 'For listTables/getRows: pagination cursor' },
|
||||||
|
sortBy: { type: 'string', required: false, description: 'For getRows: "columnName:asc" or "columnName:desc"' },
|
||||||
|
search: { type: 'string', required: false, description: 'For getRows: full-text search across string columns' },
|
||||||
|
returnType: { type: 'string', required: false, description: 'For insertRows: "count" (default), "id", or "all"' },
|
||||||
|
returnData: { type: 'boolean', required: false, description: 'For updateRows/upsertRows/deleteRows: return affected rows (default: false)' },
|
||||||
|
dryRun: { type: 'boolean', required: false, description: 'For updateRows/upsertRows/deleteRows: preview without applying (default: false)' },
|
||||||
|
},
|
||||||
|
returns: `Depends on action:
|
||||||
|
- createTable: {id, name}
|
||||||
|
- listTables: {tables, count, nextCursor?}
|
||||||
|
- getTable: Full table object with columns
|
||||||
|
- updateTable: Updated table object
|
||||||
|
- deleteTable: Success message
|
||||||
|
- getRows: {rows, count, nextCursor?}
|
||||||
|
- insertRows: Depends on returnType (count/ids/rows)
|
||||||
|
- updateRows: Update result with optional rows
|
||||||
|
- upsertRows: Upsert result with action type
|
||||||
|
- deleteRows: Delete result with optional rows`,
|
||||||
|
examples: [
|
||||||
|
'// Create a table\nn8n_manage_datatable({action: "createTable", name: "Contacts", columns: [{name: "email", type: "string"}, {name: "score", type: "number"}]})',
|
||||||
|
'// List all tables\nn8n_manage_datatable({action: "listTables"})',
|
||||||
|
'// Get table details\nn8n_manage_datatable({action: "getTable", tableId: "dt-123"})',
|
||||||
|
'// Rename a table\nn8n_manage_datatable({action: "updateTable", tableId: "dt-123", name: "New Name"})',
|
||||||
|
'// Delete a table\nn8n_manage_datatable({action: "deleteTable", tableId: "dt-123"})',
|
||||||
|
'// Get rows with filter\nn8n_manage_datatable({action: "getRows", tableId: "dt-123", filter: {filters: [{columnName: "status", condition: "eq", value: "active"}]}, limit: 50})',
|
||||||
|
'// Search rows\nn8n_manage_datatable({action: "getRows", tableId: "dt-123", search: "john", sortBy: "name:asc"})',
|
||||||
|
'// Insert rows\nn8n_manage_datatable({action: "insertRows", tableId: "dt-123", data: [{email: "a@b.com", score: 10}], returnType: "all"})',
|
||||||
|
'// Update rows (dry run)\nn8n_manage_datatable({action: "updateRows", tableId: "dt-123", filter: {filters: [{columnName: "score", condition: "lt", value: 5}]}, data: {status: "inactive"}, dryRun: true})',
|
||||||
|
'// Upsert a row\nn8n_manage_datatable({action: "upsertRows", tableId: "dt-123", filter: {filters: [{columnName: "email", condition: "eq", value: "a@b.com"}]}, data: {score: 15}, returnData: true})',
|
||||||
|
'// Delete rows\nn8n_manage_datatable({action: "deleteRows", tableId: "dt-123", filter: {filters: [{columnName: "status", condition: "eq", value: "deleted"}]}})',
|
||||||
|
],
|
||||||
|
useCases: [
|
||||||
|
'Persist structured workflow data across executions',
|
||||||
|
'Store and query lookup tables for workflow logic',
|
||||||
|
'Bulk insert records from external data sources',
|
||||||
|
'Conditionally update records matching criteria',
|
||||||
|
'Upsert to maintain unique records by key column',
|
||||||
|
'Clean up old or invalid rows with filtered delete',
|
||||||
|
'Preview changes with dryRun before modifying data',
|
||||||
|
],
|
||||||
|
performance: 'Table operations: 50-300ms. Row operations: 100-500ms depending on data size and filters.',
|
||||||
|
bestPractices: [
|
||||||
|
'Define column types upfront for schema consistency',
|
||||||
|
'Use dryRun: true before bulk updates/deletes to verify filter correctness',
|
||||||
|
'Use returnType: "count" (default) for insertRows to minimize response size',
|
||||||
|
'Use filter with specific conditions to avoid unintended bulk operations',
|
||||||
|
'Use cursor-based pagination for large result sets',
|
||||||
|
'Use sortBy for deterministic row ordering',
|
||||||
|
],
|
||||||
|
pitfalls: [
|
||||||
|
'Requires N8N_API_URL and N8N_API_KEY configured',
|
||||||
|
'Feature only available on n8n enterprise or cloud plans',
|
||||||
|
'deleteTable permanently deletes all rows — cannot be undone',
|
||||||
|
'deleteRows requires a filter — cannot delete all rows without one',
|
||||||
|
'Column types cannot be changed after table creation via API',
|
||||||
|
'updateTable can only rename the table (no column modifications via public API)',
|
||||||
|
'projectId cannot be set via the public API — use the n8n UI',
|
||||||
|
],
|
||||||
|
relatedTools: ['n8n_create_workflow', 'n8n_list_workflows', 'n8n_health_check'],
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -4,7 +4,7 @@ export const n8nUpdatePartialWorkflowDoc: ToolDocumentation = {
|
|||||||
name: 'n8n_update_partial_workflow',
|
name: 'n8n_update_partial_workflow',
|
||||||
category: 'workflow_management',
|
category: 'workflow_management',
|
||||||
essentials: {
|
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. 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, 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'],
|
keyParameters: ['id', 'operations', 'continueOnError'],
|
||||||
example: 'n8n_update_partial_workflow({id: "wf_123", operations: [{type: "rewireConnection", source: "IF", from: "Old", to: "New", branch: "true"}]})',
|
example: 'n8n_update_partial_workflow({id: "wf_123", operations: [{type: "rewireConnection", source: "IF", from: "Old", to: "New", branch: "true"}]})',
|
||||||
performance: 'Fast (50-200ms)',
|
performance: 'Fast (50-200ms)',
|
||||||
@@ -22,7 +22,8 @@ export const n8nUpdatePartialWorkflowDoc: ToolDocumentation = {
|
|||||||
'Batch AI component connections for atomic updates',
|
'Batch AI component connections for atomic updates',
|
||||||
'Auto-sanitization: ALL nodes auto-fixed during updates (operator structures, missing metadata)',
|
'Auto-sanitization: ALL nodes auto-fixed during updates (operator structures, missing metadata)',
|
||||||
'Node renames automatically update all connection references - no manual connection operations needed',
|
'Node renames automatically update all connection references - no manual connection operations needed',
|
||||||
'Activate/deactivate workflows: Use activateWorkflow/deactivateWorkflow operations (requires activatable triggers like webhook/schedule)'
|
'Activate/deactivate workflows: Use activateWorkflow/deactivateWorkflow operations (requires activatable triggers like webhook/schedule)',
|
||||||
|
'Transfer workflows between projects: Use transferWorkflow with destinationProjectId (enterprise feature)'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
full: {
|
full: {
|
||||||
@@ -55,6 +56,9 @@ export const n8nUpdatePartialWorkflowDoc: ToolDocumentation = {
|
|||||||
- **activateWorkflow**: Activate the workflow to enable automatic execution via triggers
|
- **activateWorkflow**: Activate the workflow to enable automatic execution via triggers
|
||||||
- **deactivateWorkflow**: Deactivate the workflow to prevent automatic execution
|
- **deactivateWorkflow**: Deactivate the workflow to prevent automatic execution
|
||||||
|
|
||||||
|
### Project Management Operations (1 type):
|
||||||
|
- **transferWorkflow**: Transfer the workflow to a different project. Requires \`destinationProjectId\`. Enterprise/cloud feature.
|
||||||
|
|
||||||
## Smart Parameters for Multi-Output Nodes
|
## Smart Parameters for Multi-Output Nodes
|
||||||
|
|
||||||
For **IF nodes**, use semantic 'branch' parameter instead of technical sourceIndex:
|
For **IF nodes**, use semantic 'branch' parameter instead of technical sourceIndex:
|
||||||
@@ -345,7 +349,10 @@ n8n_update_partial_workflow({
|
|||||||
'// Migrate from deprecated continueOnFail to onError\nn8n_update_partial_workflow({id: "rm2", operations: [{type: "updateNode", nodeName: "HTTP Request", updates: {continueOnFail: null, onError: "continueErrorOutput"}}]})',
|
'// Migrate from deprecated continueOnFail to onError\nn8n_update_partial_workflow({id: "rm2", operations: [{type: "updateNode", nodeName: "HTTP Request", updates: {continueOnFail: null, onError: "continueErrorOutput"}}]})',
|
||||||
'// Remove nested property\nn8n_update_partial_workflow({id: "rm3", operations: [{type: "updateNode", nodeName: "API Request", updates: {"parameters.authentication": null}}]})',
|
'// Remove nested property\nn8n_update_partial_workflow({id: "rm3", operations: [{type: "updateNode", nodeName: "API Request", updates: {"parameters.authentication": null}}]})',
|
||||||
'// Remove multiple properties\nn8n_update_partial_workflow({id: "rm4", operations: [{type: "updateNode", nodeName: "Data Processor", updates: {continueOnFail: null, alwaysOutputData: null, "parameters.legacy_option": null}}]})',
|
'// Remove multiple properties\nn8n_update_partial_workflow({id: "rm4", operations: [{type: "updateNode", nodeName: "Data Processor", updates: {continueOnFail: null, alwaysOutputData: null, "parameters.legacy_option": null}}]})',
|
||||||
'// Remove entire array property\nn8n_update_partial_workflow({id: "rm5", operations: [{type: "updateNode", nodeName: "HTTP Request", updates: {"parameters.headers": null}}]})'
|
'// Remove entire array property\nn8n_update_partial_workflow({id: "rm5", operations: [{type: "updateNode", nodeName: "HTTP Request", updates: {"parameters.headers": null}}]})',
|
||||||
|
'\n// ============ PROJECT TRANSFER EXAMPLES ============',
|
||||||
|
'// Transfer workflow to a different project\nn8n_update_partial_workflow({id: "tf1", operations: [{type: "transferWorkflow", destinationProjectId: "project-abc-123"}]})',
|
||||||
|
'// Transfer and activate in one call\nn8n_update_partial_workflow({id: "tf2", operations: [{type: "transferWorkflow", destinationProjectId: "project-abc-123"}, {type: "activateWorkflow"}]})'
|
||||||
],
|
],
|
||||||
useCases: [
|
useCases: [
|
||||||
'Rewire connections when replacing nodes',
|
'Rewire connections when replacing nodes',
|
||||||
@@ -363,7 +370,8 @@ n8n_update_partial_workflow({
|
|||||||
'Add fallback language models to AI Agents',
|
'Add fallback language models to AI Agents',
|
||||||
'Configure Vector Store retrieval systems',
|
'Configure Vector Store retrieval systems',
|
||||||
'Swap language models in existing AI workflows',
|
'Swap language models in existing AI workflows',
|
||||||
'Batch-update AI tool connections'
|
'Batch-update AI tool connections',
|
||||||
|
'Transfer workflows between team projects (enterprise)'
|
||||||
],
|
],
|
||||||
performance: 'Very fast - typically 50-200ms. Much faster than full updates as only changes are processed.',
|
performance: 'Very fast - typically 50-200ms. Much faster than full updates as only changes are processed.',
|
||||||
bestPractices: [
|
bestPractices: [
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ export const n8nManagementTools: ToolDefinition[] = [
|
|||||||
executionTimeout: { type: 'number' },
|
executionTimeout: { type: 'number' },
|
||||||
errorWorkflow: { type: 'string' }
|
errorWorkflow: { type: 'string' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
projectId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Optional project ID to create the workflow in (enterprise feature)'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
required: ['name', 'nodes', 'connections']
|
required: ['name', 'nodes', 'connections']
|
||||||
@@ -143,7 +147,7 @@ export const n8nManagementTools: ToolDefinition[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'n8n_update_partial_workflow',
|
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. See tools_documentation("n8n_update_partial_workflow", "full") for details.`,
|
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.`,
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
additionalProperties: true, // Allow any extra properties Claude Desktop might add
|
additionalProperties: true, // Allow any extra properties Claude Desktop might add
|
||||||
@@ -602,5 +606,52 @@ export const n8nManagementTools: ToolDefinition[] = [
|
|||||||
destructiveHint: false,
|
destructiveHint: false,
|
||||||
openWorldHint: true,
|
openWorldHint: true,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
name: 'n8n_manage_datatable',
|
||||||
|
description: `Manage n8n data tables and rows. Actions: createTable, listTables, getTable, updateTable, deleteTable, getRows, insertRows, updateRows, upsertRows, deleteRows. Requires n8n enterprise/cloud with data tables feature.`,
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
action: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['createTable', 'listTables', 'getTable', 'updateTable', 'deleteTable', 'getRows', 'insertRows', 'updateRows', 'upsertRows', 'deleteRows'],
|
||||||
|
description: 'Operation to perform',
|
||||||
|
},
|
||||||
|
tableId: { type: 'string', description: 'Data table ID (required for all actions except createTable and listTables)' },
|
||||||
|
name: { type: 'string', description: 'For createTable/updateTable: table name' },
|
||||||
|
columns: {
|
||||||
|
type: 'array',
|
||||||
|
description: 'For createTable: column definitions',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
name: { type: 'string' },
|
||||||
|
type: { type: 'string', enum: ['string', 'number', 'boolean', 'date', 'json'] },
|
||||||
|
},
|
||||||
|
required: ['name'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: { description: 'For insertRows: array of row objects. For updateRows/upsertRows: object with column values.' },
|
||||||
|
filter: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'For getRows/updateRows/upsertRows/deleteRows: {type?: "and"|"or", filters: [{columnName, condition, value}]}',
|
||||||
|
},
|
||||||
|
limit: { type: 'number', description: 'For listTables/getRows: max results (1-100)' },
|
||||||
|
cursor: { type: 'string', description: 'For listTables/getRows: pagination cursor' },
|
||||||
|
sortBy: { type: 'string', description: 'For getRows: "columnName:asc" or "columnName:desc"' },
|
||||||
|
search: { type: 'string', description: 'For getRows: text search across string columns' },
|
||||||
|
returnType: { type: 'string', enum: ['count', 'id', 'all'], description: 'For insertRows: what to return (default: count)' },
|
||||||
|
returnData: { type: 'boolean', description: 'For updateRows/upsertRows/deleteRows: return affected rows (default: false)' },
|
||||||
|
dryRun: { type: 'boolean', description: 'For updateRows/upsertRows/deleteRows: preview without applying (default: false)' },
|
||||||
|
},
|
||||||
|
required: ['action'],
|
||||||
|
},
|
||||||
|
annotations: {
|
||||||
|
title: 'Manage Data Tables',
|
||||||
|
readOnlyHint: false,
|
||||||
|
destructiveHint: true,
|
||||||
|
openWorldHint: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -22,6 +22,15 @@ import {
|
|||||||
SourceControlStatus,
|
SourceControlStatus,
|
||||||
SourceControlPullResult,
|
SourceControlPullResult,
|
||||||
SourceControlPushResult,
|
SourceControlPushResult,
|
||||||
|
DataTable,
|
||||||
|
DataTableColumn,
|
||||||
|
DataTableListParams,
|
||||||
|
DataTableRow,
|
||||||
|
DataTableRowListParams,
|
||||||
|
DataTableInsertRowsParams,
|
||||||
|
DataTableUpdateRowsParams,
|
||||||
|
DataTableUpsertRowParams,
|
||||||
|
DataTableDeleteRowsParams,
|
||||||
} from '../types/n8n-api';
|
} from '../types/n8n-api';
|
||||||
import { handleN8nApiError, logN8nError } from '../utils/n8n-errors';
|
import { handleN8nApiError, logN8nError } from '../utils/n8n-errors';
|
||||||
import { cleanWorkflowForCreate, cleanWorkflowForUpdate } from './n8n-validation';
|
import { cleanWorkflowForCreate, cleanWorkflowForUpdate } from './n8n-validation';
|
||||||
@@ -252,6 +261,14 @@ export class N8nApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async transferWorkflow(id: string, destinationProjectId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.client.put(`/workflows/${id}/transfer`, { destinationProjectId });
|
||||||
|
} catch (error) {
|
||||||
|
throw handleN8nApiError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async activateWorkflow(id: string): Promise<Workflow> {
|
async activateWorkflow(id: string): Promise<Workflow> {
|
||||||
try {
|
try {
|
||||||
const response = await this.client.post(`/workflows/${id}/activate`, {});
|
const response = await this.client.post(`/workflows/${id}/activate`, {});
|
||||||
@@ -574,6 +591,95 @@ export class N8nApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createDataTable(params: { name: string; columns?: DataTableColumn[] }): Promise<DataTable> {
|
||||||
|
try {
|
||||||
|
const response = await this.client.post('/data-tables', params);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw handleN8nApiError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async listDataTables(params: DataTableListParams = {}): Promise<{ data: DataTable[]; nextCursor?: string | null }> {
|
||||||
|
try {
|
||||||
|
const response = await this.client.get('/data-tables', { params });
|
||||||
|
return this.validateListResponse<DataTable>(response.data, 'data-tables');
|
||||||
|
} catch (error) {
|
||||||
|
throw handleN8nApiError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDataTable(id: string): Promise<DataTable> {
|
||||||
|
try {
|
||||||
|
const response = await this.client.get(`/data-tables/${id}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw handleN8nApiError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateDataTable(id: string, params: { name: string }): Promise<DataTable> {
|
||||||
|
try {
|
||||||
|
const response = await this.client.patch(`/data-tables/${id}`, params);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw handleN8nApiError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteDataTable(id: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.client.delete(`/data-tables/${id}`);
|
||||||
|
} catch (error) {
|
||||||
|
throw handleN8nApiError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDataTableRows(id: string, params: DataTableRowListParams = {}): Promise<{ data: DataTableRow[]; nextCursor?: string | null }> {
|
||||||
|
try {
|
||||||
|
const response = await this.client.get(`/data-tables/${id}/rows`, { params });
|
||||||
|
return this.validateListResponse<DataTableRow>(response.data, 'data-table-rows');
|
||||||
|
} catch (error) {
|
||||||
|
throw handleN8nApiError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async insertDataTableRows(id: string, params: DataTableInsertRowsParams): Promise<any> {
|
||||||
|
try {
|
||||||
|
const response = await this.client.post(`/data-tables/${id}/rows`, params);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw handleN8nApiError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateDataTableRows(id: string, params: DataTableUpdateRowsParams): Promise<any> {
|
||||||
|
try {
|
||||||
|
const response = await this.client.patch(`/data-tables/${id}/rows/update`, params);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw handleN8nApiError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async upsertDataTableRow(id: string, params: DataTableUpsertRowParams): Promise<any> {
|
||||||
|
try {
|
||||||
|
const response = await this.client.post(`/data-tables/${id}/rows/upsert`, params);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw handleN8nApiError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteDataTableRows(id: string, params: DataTableDeleteRowsParams): Promise<any> {
|
||||||
|
try {
|
||||||
|
const response = await this.client.delete(`/data-tables/${id}/rows/delete`, { params });
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw handleN8nApiError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates and normalizes n8n API list responses.
|
* Validates and normalizes n8n API list responses.
|
||||||
* Handles both modern format {data: [], nextCursor?: string} and legacy array format.
|
* Handles both modern format {data: [], nextCursor?: string} and legacy array format.
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ import {
|
|||||||
ActivateWorkflowOperation,
|
ActivateWorkflowOperation,
|
||||||
DeactivateWorkflowOperation,
|
DeactivateWorkflowOperation,
|
||||||
CleanStaleConnectionsOperation,
|
CleanStaleConnectionsOperation,
|
||||||
ReplaceConnectionsOperation
|
ReplaceConnectionsOperation,
|
||||||
|
TransferWorkflowOperation
|
||||||
} from '../types/workflow-diff';
|
} from '../types/workflow-diff';
|
||||||
import { Workflow, WorkflowNode, WorkflowConnection } from '../types/n8n-api';
|
import { Workflow, WorkflowNode, WorkflowConnection } from '../types/n8n-api';
|
||||||
import { Logger } from '../utils/logger';
|
import { Logger } from '../utils/logger';
|
||||||
@@ -54,6 +55,8 @@ export class WorkflowDiffEngine {
|
|||||||
// Track tag operations for dedicated API calls
|
// Track tag operations for dedicated API calls
|
||||||
private tagsToAdd: string[] = [];
|
private tagsToAdd: string[] = [];
|
||||||
private tagsToRemove: string[] = [];
|
private tagsToRemove: string[] = [];
|
||||||
|
// Track transfer operation for dedicated API call
|
||||||
|
private transferToProjectId: string | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply diff operations to a workflow
|
* Apply diff operations to a workflow
|
||||||
@@ -70,6 +73,7 @@ export class WorkflowDiffEngine {
|
|||||||
this.removedNodeNames.clear();
|
this.removedNodeNames.clear();
|
||||||
this.tagsToAdd = [];
|
this.tagsToAdd = [];
|
||||||
this.tagsToRemove = [];
|
this.tagsToRemove = [];
|
||||||
|
this.transferToProjectId = undefined;
|
||||||
|
|
||||||
// Clone workflow to avoid modifying original
|
// Clone workflow to avoid modifying original
|
||||||
const workflowCopy = JSON.parse(JSON.stringify(workflow));
|
const workflowCopy = JSON.parse(JSON.stringify(workflow));
|
||||||
@@ -141,6 +145,12 @@ export class WorkflowDiffEngine {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract and clean up activation flags (same as atomic mode)
|
||||||
|
const shouldActivate = (workflowCopy as any)._shouldActivate === true;
|
||||||
|
const shouldDeactivate = (workflowCopy as any)._shouldDeactivate === true;
|
||||||
|
delete (workflowCopy as any)._shouldActivate;
|
||||||
|
delete (workflowCopy as any)._shouldDeactivate;
|
||||||
|
|
||||||
const success = appliedIndices.length > 0;
|
const success = appliedIndices.length > 0;
|
||||||
return {
|
return {
|
||||||
success,
|
success,
|
||||||
@@ -151,8 +161,11 @@ export class WorkflowDiffEngine {
|
|||||||
warnings: this.warnings.length > 0 ? this.warnings : undefined,
|
warnings: this.warnings.length > 0 ? this.warnings : undefined,
|
||||||
applied: appliedIndices,
|
applied: appliedIndices,
|
||||||
failed: failedIndices,
|
failed: failedIndices,
|
||||||
|
shouldActivate: shouldActivate || undefined,
|
||||||
|
shouldDeactivate: shouldDeactivate || undefined,
|
||||||
tagsToAdd: this.tagsToAdd.length > 0 ? this.tagsToAdd : undefined,
|
tagsToAdd: this.tagsToAdd.length > 0 ? this.tagsToAdd : undefined,
|
||||||
tagsToRemove: this.tagsToRemove.length > 0 ? this.tagsToRemove : undefined
|
tagsToRemove: this.tagsToRemove.length > 0 ? this.tagsToRemove : undefined,
|
||||||
|
transferToProjectId: this.transferToProjectId || undefined
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Atomic mode: all operations must succeed
|
// Atomic mode: all operations must succeed
|
||||||
@@ -256,7 +269,8 @@ export class WorkflowDiffEngine {
|
|||||||
shouldActivate: shouldActivate || undefined,
|
shouldActivate: shouldActivate || undefined,
|
||||||
shouldDeactivate: shouldDeactivate || undefined,
|
shouldDeactivate: shouldDeactivate || undefined,
|
||||||
tagsToAdd: this.tagsToAdd.length > 0 ? this.tagsToAdd : undefined,
|
tagsToAdd: this.tagsToAdd.length > 0 ? this.tagsToAdd : undefined,
|
||||||
tagsToRemove: this.tagsToRemove.length > 0 ? this.tagsToRemove : undefined
|
tagsToRemove: this.tagsToRemove.length > 0 ? this.tagsToRemove : undefined,
|
||||||
|
transferToProjectId: this.transferToProjectId || undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -298,6 +312,8 @@ export class WorkflowDiffEngine {
|
|||||||
case 'addTag':
|
case 'addTag':
|
||||||
case 'removeTag':
|
case 'removeTag':
|
||||||
return null; // These are always valid
|
return null; // These are always valid
|
||||||
|
case 'transferWorkflow':
|
||||||
|
return this.validateTransferWorkflow(workflow, operation as TransferWorkflowOperation);
|
||||||
case 'activateWorkflow':
|
case 'activateWorkflow':
|
||||||
return this.validateActivateWorkflow(workflow, operation);
|
return this.validateActivateWorkflow(workflow, operation);
|
||||||
case 'deactivateWorkflow':
|
case 'deactivateWorkflow':
|
||||||
@@ -367,6 +383,9 @@ export class WorkflowDiffEngine {
|
|||||||
case 'replaceConnections':
|
case 'replaceConnections':
|
||||||
this.applyReplaceConnections(workflow, operation);
|
this.applyReplaceConnections(workflow, operation);
|
||||||
break;
|
break;
|
||||||
|
case 'transferWorkflow':
|
||||||
|
this.applyTransferWorkflow(workflow, operation as TransferWorkflowOperation);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -975,6 +994,18 @@ export class WorkflowDiffEngine {
|
|||||||
(workflow as any)._shouldDeactivate = true;
|
(workflow as any)._shouldDeactivate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transfer operation — uses dedicated API call (PUT /workflows/{id}/transfer)
|
||||||
|
private validateTransferWorkflow(_workflow: Workflow, operation: TransferWorkflowOperation): string | null {
|
||||||
|
if (!operation.destinationProjectId) {
|
||||||
|
return 'transferWorkflow requires a non-empty destinationProjectId string';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyTransferWorkflow(_workflow: Workflow, operation: TransferWorkflowOperation): void {
|
||||||
|
this.transferToProjectId = operation.destinationProjectId;
|
||||||
|
}
|
||||||
|
|
||||||
// Connection cleanup operation validators
|
// Connection cleanup operation validators
|
||||||
private validateCleanStaleConnections(workflow: Workflow, operation: CleanStaleConnectionsOperation): string | null {
|
private validateCleanStaleConnections(workflow: Workflow, operation: CleanStaleConnectionsOperation): string | null {
|
||||||
// This operation is always valid - it just cleans up what it finds
|
// This operation is always valid - it just cleans up what it finds
|
||||||
@@ -1128,9 +1159,10 @@ export class WorkflowDiffEngine {
|
|||||||
const connection = connectionsAtIndex[connIndex];
|
const connection = connectionsAtIndex[connIndex];
|
||||||
// Check if target node was renamed
|
// Check if target node was renamed
|
||||||
if (renames.has(connection.node)) {
|
if (renames.has(connection.node)) {
|
||||||
|
const oldTargetName = connection.node;
|
||||||
const newTargetName = renames.get(connection.node)!;
|
const newTargetName = renames.get(connection.node)!;
|
||||||
connection.node = newTargetName;
|
connection.node = newTargetName;
|
||||||
logger.debug(`Updated connection: ${sourceName}[${outputType}][${outputIndex}][${connIndex}].node: "${connection.node}" → "${newTargetName}"`);
|
logger.debug(`Updated connection: ${sourceName}[${outputType}][${outputIndex}][${connIndex}].node: "${oldTargetName}" → "${newTargetName}"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -454,4 +454,82 @@ export interface ErrorSuggestion {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
confidence: 'high' | 'medium' | 'low';
|
confidence: 'high' | 'medium' | 'low';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data Table types
|
||||||
|
export interface DataTableColumn {
|
||||||
|
name: string;
|
||||||
|
type?: 'string' | 'number' | 'boolean' | 'date' | 'json';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataTableColumnResponse {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: 'string' | 'number' | 'boolean' | 'date' | 'json';
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataTable {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
columns?: DataTableColumnResponse[];
|
||||||
|
projectId?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataTableRow {
|
||||||
|
id?: number;
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
[columnName: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataTableFilterCondition {
|
||||||
|
columnName: string;
|
||||||
|
condition: 'eq' | 'neq' | 'like' | 'ilike' | 'gt' | 'gte' | 'lt' | 'lte';
|
||||||
|
value?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataTableFilter {
|
||||||
|
type?: 'and' | 'or';
|
||||||
|
filters: DataTableFilterCondition[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataTableListParams {
|
||||||
|
limit?: number;
|
||||||
|
cursor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataTableRowListParams {
|
||||||
|
limit?: number;
|
||||||
|
cursor?: string;
|
||||||
|
filter?: string;
|
||||||
|
sortBy?: string;
|
||||||
|
search?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataTableInsertRowsParams {
|
||||||
|
data: Record<string, unknown>[];
|
||||||
|
returnType?: 'count' | 'id' | 'all';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataTableUpdateRowsParams {
|
||||||
|
filter: DataTableFilter;
|
||||||
|
data: Record<string, unknown>;
|
||||||
|
returnData?: boolean;
|
||||||
|
dryRun?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataTableUpsertRowParams {
|
||||||
|
filter: DataTableFilter;
|
||||||
|
data: Record<string, unknown>;
|
||||||
|
returnData?: boolean;
|
||||||
|
dryRun?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataTableDeleteRowsParams {
|
||||||
|
filter: string;
|
||||||
|
returnData?: boolean;
|
||||||
|
dryRun?: boolean;
|
||||||
}
|
}
|
||||||
@@ -124,6 +124,11 @@ export interface DeactivateWorkflowOperation extends DiffOperation {
|
|||||||
// No additional properties needed - just deactivates the workflow
|
// No additional properties needed - just deactivates the workflow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TransferWorkflowOperation extends DiffOperation {
|
||||||
|
type: 'transferWorkflow';
|
||||||
|
destinationProjectId: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Connection Cleanup Operations
|
// Connection Cleanup Operations
|
||||||
export interface CleanStaleConnectionsOperation extends DiffOperation {
|
export interface CleanStaleConnectionsOperation extends DiffOperation {
|
||||||
type: 'cleanStaleConnections';
|
type: 'cleanStaleConnections';
|
||||||
@@ -161,7 +166,8 @@ export type WorkflowDiffOperation =
|
|||||||
| ActivateWorkflowOperation
|
| ActivateWorkflowOperation
|
||||||
| DeactivateWorkflowOperation
|
| DeactivateWorkflowOperation
|
||||||
| CleanStaleConnectionsOperation
|
| CleanStaleConnectionsOperation
|
||||||
| ReplaceConnectionsOperation;
|
| ReplaceConnectionsOperation
|
||||||
|
| TransferWorkflowOperation;
|
||||||
|
|
||||||
// Main diff request structure
|
// Main diff request structure
|
||||||
export interface WorkflowDiffRequest {
|
export interface WorkflowDiffRequest {
|
||||||
@@ -192,6 +198,7 @@ export interface WorkflowDiffResult {
|
|||||||
shouldDeactivate?: boolean; // Flag to deactivate workflow after update (for deactivateWorkflow operation)
|
shouldDeactivate?: boolean; // Flag to deactivate workflow after update (for deactivateWorkflow operation)
|
||||||
tagsToAdd?: string[];
|
tagsToAdd?: string[];
|
||||||
tagsToRemove?: string[];
|
tagsToRemove?: string[];
|
||||||
|
transferToProjectId?: string; // For transferWorkflow operation - uses dedicated API call
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper type for node reference (supports both ID and name)
|
// Helper type for node reference (supports both ID and name)
|
||||||
|
|||||||
727
tests/unit/mcp/handlers-manage-datatable.test.ts
Normal file
727
tests/unit/mcp/handlers-manage-datatable.test.ts
Normal file
@@ -0,0 +1,727 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { N8nApiClient } from '@/services/n8n-api-client';
|
||||||
|
import { N8nApiError } from '@/utils/n8n-errors';
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
vi.mock('@/services/n8n-api-client');
|
||||||
|
vi.mock('@/config/n8n-api', () => ({
|
||||||
|
getN8nApiConfig: vi.fn(),
|
||||||
|
}));
|
||||||
|
vi.mock('@/services/n8n-validation', () => ({
|
||||||
|
validateWorkflowStructure: vi.fn(),
|
||||||
|
hasWebhookTrigger: vi.fn(),
|
||||||
|
getWebhookUrl: vi.fn(),
|
||||||
|
}));
|
||||||
|
vi.mock('@/utils/logger', () => ({
|
||||||
|
logger: {
|
||||||
|
info: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
debug: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
},
|
||||||
|
Logger: vi.fn().mockImplementation(() => ({
|
||||||
|
info: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
debug: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
})),
|
||||||
|
LogLevel: {
|
||||||
|
ERROR: 0,
|
||||||
|
WARN: 1,
|
||||||
|
INFO: 2,
|
||||||
|
DEBUG: 3,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Data Table Handlers (n8n_manage_datatable)', () => {
|
||||||
|
let mockApiClient: any;
|
||||||
|
let handlers: any;
|
||||||
|
let getN8nApiConfig: any;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
// Setup mock API client with all data table methods
|
||||||
|
mockApiClient = {
|
||||||
|
createWorkflow: vi.fn(),
|
||||||
|
getWorkflow: vi.fn(),
|
||||||
|
updateWorkflow: vi.fn(),
|
||||||
|
deleteWorkflow: vi.fn(),
|
||||||
|
listWorkflows: vi.fn(),
|
||||||
|
triggerWebhook: vi.fn(),
|
||||||
|
getExecution: vi.fn(),
|
||||||
|
listExecutions: vi.fn(),
|
||||||
|
deleteExecution: vi.fn(),
|
||||||
|
healthCheck: vi.fn(),
|
||||||
|
createDataTable: vi.fn(),
|
||||||
|
listDataTables: vi.fn(),
|
||||||
|
getDataTable: vi.fn(),
|
||||||
|
updateDataTable: vi.fn(),
|
||||||
|
deleteDataTable: vi.fn(),
|
||||||
|
getDataTableRows: vi.fn(),
|
||||||
|
insertDataTableRows: vi.fn(),
|
||||||
|
updateDataTableRows: vi.fn(),
|
||||||
|
upsertDataTableRow: vi.fn(),
|
||||||
|
deleteDataTableRows: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Import mocked modules
|
||||||
|
getN8nApiConfig = (await import('@/config/n8n-api')).getN8nApiConfig;
|
||||||
|
|
||||||
|
// Mock the API config
|
||||||
|
vi.mocked(getN8nApiConfig).mockReturnValue({
|
||||||
|
baseUrl: 'https://n8n.test.com',
|
||||||
|
apiKey: 'test-key',
|
||||||
|
timeout: 30000,
|
||||||
|
maxRetries: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock the N8nApiClient constructor
|
||||||
|
vi.mocked(N8nApiClient).mockImplementation(() => mockApiClient);
|
||||||
|
|
||||||
|
// Import handlers module after setting up mocks
|
||||||
|
handlers = await import('@/mcp/handlers-n8n-manager');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (handlers) {
|
||||||
|
const clientGetter = handlers.getN8nApiClient;
|
||||||
|
if (clientGetter) {
|
||||||
|
vi.mocked(getN8nApiConfig).mockReturnValue(null);
|
||||||
|
clientGetter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// handleCreateTable
|
||||||
|
// ========================================================================
|
||||||
|
describe('handleCreateTable', () => {
|
||||||
|
it('should create data table with name and columns successfully', async () => {
|
||||||
|
const createdTable = {
|
||||||
|
id: 'dt-123',
|
||||||
|
name: 'My Data Table',
|
||||||
|
columns: [
|
||||||
|
{ id: 'col-1', name: 'email', type: 'string', index: 0 },
|
||||||
|
{ id: 'col-2', name: 'age', type: 'number', index: 1 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
mockApiClient.createDataTable.mockResolvedValue(createdTable);
|
||||||
|
|
||||||
|
const result = await handlers.handleCreateTable({
|
||||||
|
name: 'My Data Table',
|
||||||
|
columns: [
|
||||||
|
{ name: 'email', type: 'string' },
|
||||||
|
{ name: 'age', type: 'number' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
data: { id: 'dt-123', name: 'My Data Table' },
|
||||||
|
message: 'Data table "My Data Table" created with ID: dt-123',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockApiClient.createDataTable).toHaveBeenCalledWith({
|
||||||
|
name: 'My Data Table',
|
||||||
|
columns: [
|
||||||
|
{ name: 'email', type: 'string' },
|
||||||
|
{ name: 'age', type: 'number' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create data table with name only (no columns)', async () => {
|
||||||
|
const createdTable = {
|
||||||
|
id: 'dt-456',
|
||||||
|
name: 'Empty Table',
|
||||||
|
};
|
||||||
|
|
||||||
|
mockApiClient.createDataTable.mockResolvedValue(createdTable);
|
||||||
|
|
||||||
|
const result = await handlers.handleCreateTable({
|
||||||
|
name: 'Empty Table',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
data: { id: 'dt-456', name: 'Empty Table' },
|
||||||
|
message: 'Data table "Empty Table" created with ID: dt-456',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockApiClient.createDataTable).toHaveBeenCalledWith({
|
||||||
|
name: 'Empty Table',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return error when API returns empty response (null)', async () => {
|
||||||
|
mockApiClient.createDataTable.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await handlers.handleCreateTable({
|
||||||
|
name: 'Ghost Table',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
error: 'Data table creation failed: n8n API returned an empty or invalid response',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return error when API call fails', async () => {
|
||||||
|
const apiError = new Error('Data table creation failed on the server');
|
||||||
|
mockApiClient.createDataTable.mockRejectedValue(apiError);
|
||||||
|
|
||||||
|
const result = await handlers.handleCreateTable({
|
||||||
|
name: 'Broken Table',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
error: 'Data table creation failed on the server',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Zod validation error when name is missing', async () => {
|
||||||
|
const result = await handlers.handleCreateTable({});
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Invalid input');
|
||||||
|
expect(result.details).toHaveProperty('errors');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return error when n8n API is not configured', async () => {
|
||||||
|
vi.mocked(getN8nApiConfig).mockReturnValue(null);
|
||||||
|
|
||||||
|
const result = await handlers.handleCreateTable({
|
||||||
|
name: 'Test Table',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
error: 'n8n API not configured. Please set N8N_API_URL and N8N_API_KEY environment variables.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return structured error for N8nApiError', async () => {
|
||||||
|
const apiError = new N8nApiError('Feature not available', 402, 'PAYMENT_REQUIRED', { plan: 'enterprise' });
|
||||||
|
mockApiClient.createDataTable.mockRejectedValue(apiError);
|
||||||
|
|
||||||
|
const result = await handlers.handleCreateTable({
|
||||||
|
name: 'Enterprise Table',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBeDefined();
|
||||||
|
expect(result.code).toBe('PAYMENT_REQUIRED');
|
||||||
|
expect(result.details).toEqual({ plan: 'enterprise' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Unknown error when a non-Error value is thrown', async () => {
|
||||||
|
mockApiClient.createDataTable.mockRejectedValue('string-error');
|
||||||
|
|
||||||
|
const result = await handlers.handleCreateTable({
|
||||||
|
name: 'Error Table',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
error: 'Unknown error occurred',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// handleListTables
|
||||||
|
// ========================================================================
|
||||||
|
describe('handleListTables', () => {
|
||||||
|
it('should list tables successfully', async () => {
|
||||||
|
const tables = [
|
||||||
|
{ id: 'dt-1', name: 'Table One' },
|
||||||
|
{ id: 'dt-2', name: 'Table Two' },
|
||||||
|
];
|
||||||
|
mockApiClient.listDataTables.mockResolvedValue({ data: tables, nextCursor: null });
|
||||||
|
|
||||||
|
const result = await handlers.handleListTables({});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
tables,
|
||||||
|
count: 2,
|
||||||
|
nextCursor: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty list when no tables exist', async () => {
|
||||||
|
mockApiClient.listDataTables.mockResolvedValue({ data: [], nextCursor: null });
|
||||||
|
|
||||||
|
const result = await handlers.handleListTables({});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
tables: [],
|
||||||
|
count: 0,
|
||||||
|
nextCursor: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass pagination params (limit, cursor)', async () => {
|
||||||
|
mockApiClient.listDataTables.mockResolvedValue({
|
||||||
|
data: [{ id: 'dt-3', name: 'Page Two' }],
|
||||||
|
nextCursor: 'cursor-next',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await handlers.handleListTables({ limit: 10, cursor: 'cursor-abc' });
|
||||||
|
|
||||||
|
expect(mockApiClient.listDataTables).toHaveBeenCalledWith({ limit: 10, cursor: 'cursor-abc' });
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.data.nextCursor).toBe('cursor-next');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle API error', async () => {
|
||||||
|
mockApiClient.listDataTables.mockRejectedValue(new Error('Server down'));
|
||||||
|
|
||||||
|
const result = await handlers.handleListTables({});
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Server down');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// handleGetTable
|
||||||
|
// ========================================================================
|
||||||
|
describe('handleGetTable', () => {
|
||||||
|
it('should get table successfully', async () => {
|
||||||
|
const table = { id: 'dt-1', name: 'My Table', columns: [] };
|
||||||
|
mockApiClient.getDataTable.mockResolvedValue(table);
|
||||||
|
|
||||||
|
const result = await handlers.handleGetTable({ tableId: 'dt-1' });
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
data: table,
|
||||||
|
});
|
||||||
|
expect(mockApiClient.getDataTable).toHaveBeenCalledWith('dt-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return error on 404', async () => {
|
||||||
|
const notFoundError = new N8nApiError('Data table not found', 404, 'NOT_FOUND');
|
||||||
|
mockApiClient.getDataTable.mockRejectedValue(notFoundError);
|
||||||
|
|
||||||
|
const result = await handlers.handleGetTable({ tableId: 'dt-nonexistent' });
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBeDefined();
|
||||||
|
expect(result.code).toBe('NOT_FOUND');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Zod validation error when tableId is missing', async () => {
|
||||||
|
const result = await handlers.handleGetTable({});
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Invalid input');
|
||||||
|
expect(result.details).toHaveProperty('errors');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// handleUpdateTable
|
||||||
|
// ========================================================================
|
||||||
|
describe('handleUpdateTable', () => {
|
||||||
|
it('should rename table successfully', async () => {
|
||||||
|
const updatedTable = { id: 'dt-1', name: 'Renamed Table' };
|
||||||
|
mockApiClient.updateDataTable.mockResolvedValue(updatedTable);
|
||||||
|
|
||||||
|
const result = await handlers.handleUpdateTable({ tableId: 'dt-1', name: 'Renamed Table' });
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
data: updatedTable,
|
||||||
|
message: 'Data table renamed to "Renamed Table"',
|
||||||
|
});
|
||||||
|
expect(mockApiClient.updateDataTable).toHaveBeenCalledWith('dt-1', { name: 'Renamed Table' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Zod validation error when tableId is missing', async () => {
|
||||||
|
const result = await handlers.handleUpdateTable({ name: 'New Name' });
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Invalid input');
|
||||||
|
expect(result.details).toHaveProperty('errors');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return error when API call fails', async () => {
|
||||||
|
mockApiClient.updateDataTable.mockRejectedValue(new Error('Update failed'));
|
||||||
|
|
||||||
|
const result = await handlers.handleUpdateTable({ tableId: 'dt-1', name: 'New Name' });
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Update failed');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// handleDeleteTable
|
||||||
|
// ========================================================================
|
||||||
|
describe('handleDeleteTable', () => {
|
||||||
|
it('should delete table successfully', async () => {
|
||||||
|
mockApiClient.deleteDataTable.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
const result = await handlers.handleDeleteTable({ tableId: 'dt-1' });
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
message: 'Data table dt-1 deleted successfully',
|
||||||
|
});
|
||||||
|
expect(mockApiClient.deleteDataTable).toHaveBeenCalledWith('dt-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return error on 404', async () => {
|
||||||
|
const notFoundError = new N8nApiError('Data table not found', 404, 'NOT_FOUND');
|
||||||
|
mockApiClient.deleteDataTable.mockRejectedValue(notFoundError);
|
||||||
|
|
||||||
|
const result = await handlers.handleDeleteTable({ tableId: 'dt-nonexistent' });
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBeDefined();
|
||||||
|
expect(result.code).toBe('NOT_FOUND');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// handleGetRows
|
||||||
|
// ========================================================================
|
||||||
|
describe('handleGetRows', () => {
|
||||||
|
it('should get rows with default params', async () => {
|
||||||
|
const rows = [
|
||||||
|
{ id: 1, email: 'a@b.com', score: 10 },
|
||||||
|
{ id: 2, email: 'c@d.com', score: 20 },
|
||||||
|
];
|
||||||
|
mockApiClient.getDataTableRows.mockResolvedValue({ data: rows, nextCursor: null });
|
||||||
|
|
||||||
|
const result = await handlers.handleGetRows({ tableId: 'dt-1' });
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
rows,
|
||||||
|
count: 2,
|
||||||
|
nextCursor: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(mockApiClient.getDataTableRows).toHaveBeenCalledWith('dt-1', {});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass filter, sort, and search params', async () => {
|
||||||
|
mockApiClient.getDataTableRows.mockResolvedValue({ data: [], nextCursor: null });
|
||||||
|
|
||||||
|
await handlers.handleGetRows({
|
||||||
|
tableId: 'dt-1',
|
||||||
|
limit: 50,
|
||||||
|
sortBy: 'name:asc',
|
||||||
|
search: 'john',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockApiClient.getDataTableRows).toHaveBeenCalledWith('dt-1', {
|
||||||
|
limit: 50,
|
||||||
|
sortBy: 'name:asc',
|
||||||
|
search: 'john',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should serialize object filter to JSON string', async () => {
|
||||||
|
mockApiClient.getDataTableRows.mockResolvedValue({ data: [], nextCursor: null });
|
||||||
|
|
||||||
|
const objectFilter = {
|
||||||
|
type: 'and' as const,
|
||||||
|
filters: [{ columnName: 'status', condition: 'eq' as const, value: 'active' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
await handlers.handleGetRows({
|
||||||
|
tableId: 'dt-1',
|
||||||
|
filter: objectFilter,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockApiClient.getDataTableRows).toHaveBeenCalledWith('dt-1', {
|
||||||
|
filter: JSON.stringify(objectFilter),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass through string filter as-is', async () => {
|
||||||
|
mockApiClient.getDataTableRows.mockResolvedValue({ data: [], nextCursor: null });
|
||||||
|
|
||||||
|
await handlers.handleGetRows({
|
||||||
|
tableId: 'dt-1',
|
||||||
|
filter: '{"type":"and","filters":[]}',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockApiClient.getDataTableRows).toHaveBeenCalledWith('dt-1', {
|
||||||
|
filter: '{"type":"and","filters":[]}',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// handleInsertRows
|
||||||
|
// ========================================================================
|
||||||
|
describe('handleInsertRows', () => {
|
||||||
|
it('should insert rows successfully', async () => {
|
||||||
|
const insertResult = { insertedCount: 2, ids: [1, 2] };
|
||||||
|
mockApiClient.insertDataTableRows.mockResolvedValue(insertResult);
|
||||||
|
|
||||||
|
const result = await handlers.handleInsertRows({
|
||||||
|
tableId: 'dt-1',
|
||||||
|
data: [
|
||||||
|
{ email: 'a@b.com', score: 10 },
|
||||||
|
{ email: 'c@d.com', score: 20 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
data: insertResult,
|
||||||
|
message: 'Rows inserted into data table dt-1',
|
||||||
|
});
|
||||||
|
expect(mockApiClient.insertDataTableRows).toHaveBeenCalledWith('dt-1', {
|
||||||
|
data: [
|
||||||
|
{ email: 'a@b.com', score: 10 },
|
||||||
|
{ email: 'c@d.com', score: 20 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass returnType to the API client', async () => {
|
||||||
|
const insertResult = [{ id: 1, email: 'a@b.com', score: 10 }];
|
||||||
|
mockApiClient.insertDataTableRows.mockResolvedValue(insertResult);
|
||||||
|
|
||||||
|
const result = await handlers.handleInsertRows({
|
||||||
|
tableId: 'dt-1',
|
||||||
|
data: [{ email: 'a@b.com', score: 10 }],
|
||||||
|
returnType: 'all',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.data).toEqual(insertResult);
|
||||||
|
expect(mockApiClient.insertDataTableRows).toHaveBeenCalledWith('dt-1', {
|
||||||
|
data: [{ email: 'a@b.com', score: 10 }],
|
||||||
|
returnType: 'all',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Zod validation error when data is empty array', async () => {
|
||||||
|
const result = await handlers.handleInsertRows({
|
||||||
|
tableId: 'dt-1',
|
||||||
|
data: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Invalid input');
|
||||||
|
expect(result.details).toHaveProperty('errors');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// handleUpdateRows
|
||||||
|
// ========================================================================
|
||||||
|
describe('handleUpdateRows', () => {
|
||||||
|
it('should update rows successfully', async () => {
|
||||||
|
const updateResult = { updatedCount: 3 };
|
||||||
|
mockApiClient.updateDataTableRows.mockResolvedValue(updateResult);
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
type: 'and' as const,
|
||||||
|
filters: [{ columnName: 'status', condition: 'eq' as const, value: 'inactive' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await handlers.handleUpdateRows({
|
||||||
|
tableId: 'dt-1',
|
||||||
|
filter,
|
||||||
|
data: { status: 'active' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
data: updateResult,
|
||||||
|
message: 'Rows updated successfully',
|
||||||
|
});
|
||||||
|
expect(mockApiClient.updateDataTableRows).toHaveBeenCalledWith('dt-1', {
|
||||||
|
filter,
|
||||||
|
data: { status: 'active' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support dryRun mode', async () => {
|
||||||
|
const dryRunResult = { matchedCount: 5 };
|
||||||
|
mockApiClient.updateDataTableRows.mockResolvedValue(dryRunResult);
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
filters: [{ columnName: 'score', condition: 'lt' as const, value: 5 }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await handlers.handleUpdateRows({
|
||||||
|
tableId: 'dt-1',
|
||||||
|
filter,
|
||||||
|
data: { status: 'low' },
|
||||||
|
dryRun: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.message).toBe('Dry run: rows matched (no changes applied)');
|
||||||
|
expect(mockApiClient.updateDataTableRows).toHaveBeenCalledWith('dt-1', {
|
||||||
|
filter: { type: 'and', ...filter },
|
||||||
|
data: { status: 'low' },
|
||||||
|
dryRun: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return error on API failure', async () => {
|
||||||
|
mockApiClient.updateDataTableRows.mockRejectedValue(new Error('Conflict'));
|
||||||
|
|
||||||
|
const result = await handlers.handleUpdateRows({
|
||||||
|
tableId: 'dt-1',
|
||||||
|
filter: { filters: [{ columnName: 'id', condition: 'eq' as const, value: 1 }] },
|
||||||
|
data: { name: 'test' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Conflict');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// handleUpsertRows
|
||||||
|
// ========================================================================
|
||||||
|
describe('handleUpsertRows', () => {
|
||||||
|
it('should upsert row successfully', async () => {
|
||||||
|
const upsertResult = { action: 'updated', row: { id: 1, email: 'a@b.com', score: 15 } };
|
||||||
|
mockApiClient.upsertDataTableRow.mockResolvedValue(upsertResult);
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
filters: [{ columnName: 'email', condition: 'eq' as const, value: 'a@b.com' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await handlers.handleUpsertRows({
|
||||||
|
tableId: 'dt-1',
|
||||||
|
filter,
|
||||||
|
data: { score: 15 },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
data: upsertResult,
|
||||||
|
message: 'Row upserted successfully',
|
||||||
|
});
|
||||||
|
expect(mockApiClient.upsertDataTableRow).toHaveBeenCalledWith('dt-1', {
|
||||||
|
filter: { type: 'and', ...filter },
|
||||||
|
data: { score: 15 },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support dryRun mode', async () => {
|
||||||
|
const dryRunResult = { action: 'would_update', matchedRows: 1 };
|
||||||
|
mockApiClient.upsertDataTableRow.mockResolvedValue(dryRunResult);
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
filters: [{ columnName: 'email', condition: 'eq' as const, value: 'a@b.com' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await handlers.handleUpsertRows({
|
||||||
|
tableId: 'dt-1',
|
||||||
|
filter,
|
||||||
|
data: { score: 20 },
|
||||||
|
dryRun: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.message).toBe('Dry run: upsert previewed (no changes applied)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return error on API failure', async () => {
|
||||||
|
const apiError = new N8nApiError('Server error', 500, 'INTERNAL_ERROR');
|
||||||
|
mockApiClient.upsertDataTableRow.mockRejectedValue(apiError);
|
||||||
|
|
||||||
|
const result = await handlers.handleUpsertRows({
|
||||||
|
tableId: 'dt-1',
|
||||||
|
filter: { filters: [{ columnName: 'id', condition: 'eq' as const, value: 1 }] },
|
||||||
|
data: { name: 'test' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBeDefined();
|
||||||
|
expect(result.code).toBe('INTERNAL_ERROR');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// handleDeleteRows
|
||||||
|
// ========================================================================
|
||||||
|
describe('handleDeleteRows', () => {
|
||||||
|
it('should delete rows successfully', async () => {
|
||||||
|
const deleteResult = { deletedCount: 2 };
|
||||||
|
mockApiClient.deleteDataTableRows.mockResolvedValue(deleteResult);
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
filters: [{ columnName: 'status', condition: 'eq' as const, value: 'deleted' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await handlers.handleDeleteRows({
|
||||||
|
tableId: 'dt-1',
|
||||||
|
filter,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
data: deleteResult,
|
||||||
|
message: 'Rows deleted successfully',
|
||||||
|
});
|
||||||
|
expect(mockApiClient.deleteDataTableRows).toHaveBeenCalledWith('dt-1', {
|
||||||
|
filter: JSON.stringify({ type: 'and', ...filter }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should serialize filter object to JSON string for API call', async () => {
|
||||||
|
mockApiClient.deleteDataTableRows.mockResolvedValue({ deletedCount: 1 });
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
type: 'or' as const,
|
||||||
|
filters: [
|
||||||
|
{ columnName: 'score', condition: 'lt' as const, value: 0 },
|
||||||
|
{ columnName: 'status', condition: 'eq' as const, value: 'spam' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
await handlers.handleDeleteRows({ tableId: 'dt-1', filter });
|
||||||
|
|
||||||
|
expect(mockApiClient.deleteDataTableRows).toHaveBeenCalledWith('dt-1', {
|
||||||
|
filter: JSON.stringify(filter),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support dryRun mode', async () => {
|
||||||
|
const dryRunResult = { matchedCount: 4 };
|
||||||
|
mockApiClient.deleteDataTableRows.mockResolvedValue(dryRunResult);
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
filters: [{ columnName: 'active', condition: 'eq' as const, value: false }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await handlers.handleDeleteRows({
|
||||||
|
tableId: 'dt-1',
|
||||||
|
filter,
|
||||||
|
dryRun: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.message).toBe('Dry run: rows matched for deletion (no changes applied)');
|
||||||
|
expect(mockApiClient.deleteDataTableRows).toHaveBeenCalledWith('dt-1', {
|
||||||
|
filter: JSON.stringify({ type: 'and', ...filter }),
|
||||||
|
dryRun: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -631,6 +631,27 @@ describe('handlers-n8n-manager', () => {
|
|||||||
expect(result.details.errors[0]).toContain('Webhook');
|
expect(result.details.errors[0]).toContain('Webhook');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should pass projectId to API when provided', async () => {
|
||||||
|
const testWorkflow = createTestWorkflow();
|
||||||
|
const input = {
|
||||||
|
name: 'Test Workflow',
|
||||||
|
nodes: testWorkflow.nodes,
|
||||||
|
connections: testWorkflow.connections,
|
||||||
|
projectId: 'project-abc-123',
|
||||||
|
};
|
||||||
|
|
||||||
|
mockApiClient.createWorkflow.mockResolvedValue(testWorkflow);
|
||||||
|
|
||||||
|
const result = await handlers.handleCreateWorkflow(input);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(mockApiClient.createWorkflow).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
projectId: 'project-abc-123',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('handleGetWorkflow', () => {
|
describe('handleGetWorkflow', () => {
|
||||||
@@ -1081,10 +1102,10 @@ describe('handlers-n8n-manager', () => {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
managementTools: {
|
managementTools: {
|
||||||
count: 13,
|
count: 14,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
totalAvailable: 20,
|
totalAvailable: 21,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ describe('handlers-workflow-diff', () => {
|
|||||||
listTags: vi.fn().mockResolvedValue({ data: [] }),
|
listTags: vi.fn().mockResolvedValue({ data: [] }),
|
||||||
createTag: vi.fn(),
|
createTag: vi.fn(),
|
||||||
updateWorkflowTags: vi.fn().mockResolvedValue([]),
|
updateWorkflowTags: vi.fn().mockResolvedValue([]),
|
||||||
|
transferWorkflow: vi.fn().mockResolvedValue(undefined),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Setup mock diff engine
|
// Setup mock diff engine
|
||||||
@@ -1002,6 +1003,186 @@ describe('handlers-workflow-diff', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Project Transfer via Dedicated API', () => {
|
||||||
|
it('should call transferWorkflow when diffResult has transferToProjectId', async () => {
|
||||||
|
const testWorkflow = createTestWorkflow();
|
||||||
|
const updatedWorkflow = { ...testWorkflow };
|
||||||
|
|
||||||
|
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||||
|
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
workflow: updatedWorkflow,
|
||||||
|
operationsApplied: 1,
|
||||||
|
message: 'Success',
|
||||||
|
errors: [],
|
||||||
|
transferToProjectId: 'project-abc-123',
|
||||||
|
});
|
||||||
|
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
|
||||||
|
|
||||||
|
const result = await handleUpdatePartialWorkflow({
|
||||||
|
id: 'test-workflow-id',
|
||||||
|
operations: [{ type: 'transferWorkflow', destinationProjectId: 'project-abc-123' }],
|
||||||
|
}, mockRepository);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(mockApiClient.transferWorkflow).toHaveBeenCalledWith('test-workflow-id', 'project-abc-123');
|
||||||
|
expect(result.message).toContain('transferred to project');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT call transferWorkflow when transferToProjectId is absent', async () => {
|
||||||
|
const testWorkflow = createTestWorkflow();
|
||||||
|
const updatedWorkflow = { ...testWorkflow };
|
||||||
|
|
||||||
|
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||||
|
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
workflow: updatedWorkflow,
|
||||||
|
operationsApplied: 1,
|
||||||
|
message: 'Success',
|
||||||
|
errors: [],
|
||||||
|
});
|
||||||
|
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
|
||||||
|
|
||||||
|
await handleUpdatePartialWorkflow({
|
||||||
|
id: 'test-workflow-id',
|
||||||
|
operations: [{ type: 'updateName', name: 'New Name' }],
|
||||||
|
}, mockRepository);
|
||||||
|
|
||||||
|
expect(mockApiClient.transferWorkflow).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return success false with saved true when transfer fails', async () => {
|
||||||
|
const testWorkflow = createTestWorkflow();
|
||||||
|
const updatedWorkflow = { ...testWorkflow };
|
||||||
|
|
||||||
|
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||||
|
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
workflow: updatedWorkflow,
|
||||||
|
operationsApplied: 1,
|
||||||
|
message: 'Success',
|
||||||
|
errors: [],
|
||||||
|
transferToProjectId: 'project-bad-id',
|
||||||
|
});
|
||||||
|
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
|
||||||
|
mockApiClient.transferWorkflow.mockRejectedValue(new Error('Project not found'));
|
||||||
|
|
||||||
|
const result = await handleUpdatePartialWorkflow({
|
||||||
|
id: 'test-workflow-id',
|
||||||
|
operations: [{ type: 'transferWorkflow', destinationProjectId: 'project-bad-id' }],
|
||||||
|
}, mockRepository);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.saved).toBe(true);
|
||||||
|
expect(result.error).toBe('Workflow updated successfully but project transfer failed');
|
||||||
|
expect(result.details).toEqual({
|
||||||
|
workflowUpdated: true,
|
||||||
|
transferError: 'Project not found',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Unknown error when non-Error value is thrown during transfer', async () => {
|
||||||
|
const testWorkflow = createTestWorkflow();
|
||||||
|
const updatedWorkflow = { ...testWorkflow };
|
||||||
|
|
||||||
|
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||||
|
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
workflow: updatedWorkflow,
|
||||||
|
operationsApplied: 1,
|
||||||
|
message: 'Success',
|
||||||
|
errors: [],
|
||||||
|
transferToProjectId: 'project-unknown',
|
||||||
|
});
|
||||||
|
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
|
||||||
|
mockApiClient.transferWorkflow.mockRejectedValue('string error');
|
||||||
|
|
||||||
|
const result = await handleUpdatePartialWorkflow({
|
||||||
|
id: 'test-workflow-id',
|
||||||
|
operations: [{ type: 'transferWorkflow', destinationProjectId: 'project-unknown' }],
|
||||||
|
}, mockRepository);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.saved).toBe(true);
|
||||||
|
expect(result.details).toEqual({
|
||||||
|
workflowUpdated: true,
|
||||||
|
transferError: 'Unknown error',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call transferWorkflow BEFORE activateWorkflow', async () => {
|
||||||
|
const testWorkflow = createTestWorkflow({ active: false });
|
||||||
|
const updatedWorkflow = { ...testWorkflow, active: false };
|
||||||
|
const activatedWorkflow = { ...testWorkflow, active: true };
|
||||||
|
|
||||||
|
const callOrder: string[] = [];
|
||||||
|
|
||||||
|
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||||
|
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
workflow: updatedWorkflow,
|
||||||
|
operationsApplied: 2,
|
||||||
|
message: 'Success',
|
||||||
|
errors: [],
|
||||||
|
transferToProjectId: 'project-target',
|
||||||
|
shouldActivate: true,
|
||||||
|
});
|
||||||
|
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
|
||||||
|
mockApiClient.transferWorkflow.mockImplementation(async () => {
|
||||||
|
callOrder.push('transfer');
|
||||||
|
});
|
||||||
|
mockApiClient.activateWorkflow = vi.fn().mockImplementation(async () => {
|
||||||
|
callOrder.push('activate');
|
||||||
|
return activatedWorkflow;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await handleUpdatePartialWorkflow({
|
||||||
|
id: 'test-workflow-id',
|
||||||
|
operations: [
|
||||||
|
{ type: 'transferWorkflow', destinationProjectId: 'project-target' },
|
||||||
|
{ type: 'activateWorkflow' },
|
||||||
|
],
|
||||||
|
}, mockRepository);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(mockApiClient.transferWorkflow).toHaveBeenCalledWith('test-workflow-id', 'project-target');
|
||||||
|
expect(mockApiClient.activateWorkflow).toHaveBeenCalledWith('test-workflow-id');
|
||||||
|
expect(callOrder).toEqual(['transfer', 'activate']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip activation when transfer fails', async () => {
|
||||||
|
const testWorkflow = createTestWorkflow({ active: false });
|
||||||
|
const updatedWorkflow = { ...testWorkflow, active: false };
|
||||||
|
|
||||||
|
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||||
|
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
workflow: updatedWorkflow,
|
||||||
|
operationsApplied: 2,
|
||||||
|
message: 'Success',
|
||||||
|
errors: [],
|
||||||
|
transferToProjectId: 'project-fail',
|
||||||
|
shouldActivate: true,
|
||||||
|
});
|
||||||
|
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
|
||||||
|
mockApiClient.transferWorkflow.mockRejectedValue(new Error('Transfer denied'));
|
||||||
|
mockApiClient.activateWorkflow = vi.fn();
|
||||||
|
|
||||||
|
const result = await handleUpdatePartialWorkflow({
|
||||||
|
id: 'test-workflow-id',
|
||||||
|
operations: [
|
||||||
|
{ type: 'transferWorkflow', destinationProjectId: 'project-fail' },
|
||||||
|
{ type: 'activateWorkflow' },
|
||||||
|
],
|
||||||
|
}, mockRepository);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.saved).toBe(true);
|
||||||
|
expect(result.error).toBe('Workflow updated successfully but project transfer failed');
|
||||||
|
expect(mockApiClient.activateWorkflow).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('field name normalization', () => {
|
describe('field name normalization', () => {
|
||||||
it('should normalize "name" to "nodeName" for updateNode operations', async () => {
|
it('should normalize "name" to "nodeName" for updateNode operations', async () => {
|
||||||
const testWorkflow = createTestWorkflow();
|
const testWorkflow = createTestWorkflow();
|
||||||
@@ -1119,4 +1300,4 @@ describe('handlers-workflow-diff', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -542,6 +542,9 @@ describe('Parameter Validation', () => {
|
|||||||
await expect(server.testExecuteTool('n8n_test_workflow', {}))
|
await expect(server.testExecuteTool('n8n_test_workflow', {}))
|
||||||
.rejects.toThrow('Missing required parameters for n8n_test_workflow: workflowId');
|
.rejects.toThrow('Missing required parameters for n8n_test_workflow: workflowId');
|
||||||
|
|
||||||
|
await expect(server.testExecuteTool('n8n_manage_datatable', {}))
|
||||||
|
.rejects.toThrow('n8n_manage_datatable: Validation failed:\n • action: action is required');
|
||||||
|
|
||||||
for (const tool of n8nToolsWithRequiredParams) {
|
for (const tool of n8nToolsWithRequiredParams) {
|
||||||
await expect(server.testExecuteTool(tool.name, tool.args))
|
await expect(server.testExecuteTool(tool.name, tool.args))
|
||||||
.rejects.toThrow(tool.expected);
|
.rejects.toThrow(tool.expected);
|
||||||
|
|||||||
@@ -1250,6 +1250,413 @@ describe('N8nApiClient', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('transferWorkflow', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
client = new N8nApiClient(defaultConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transfer workflow successfully via PUT', async () => {
|
||||||
|
mockAxiosInstance.put.mockResolvedValue({ data: undefined });
|
||||||
|
|
||||||
|
await client.transferWorkflow('123', 'project-456');
|
||||||
|
|
||||||
|
expect(mockAxiosInstance.put).toHaveBeenCalledWith(
|
||||||
|
'/workflows/123/transfer',
|
||||||
|
{ destinationProjectId: 'project-456' }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw N8nNotFoundError on 404', async () => {
|
||||||
|
const error = {
|
||||||
|
message: 'Request failed',
|
||||||
|
response: { status: 404, data: { message: 'Workflow not found' } }
|
||||||
|
};
|
||||||
|
await mockAxiosInstance.simulateError('put', error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.transferWorkflow('123', 'project-456');
|
||||||
|
expect.fail('Should have thrown an error');
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).toBeInstanceOf(N8nNotFoundError);
|
||||||
|
expect((err as N8nNotFoundError).message).toContain('not found');
|
||||||
|
expect((err as N8nNotFoundError).statusCode).toBe(404);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw appropriate error on 403 forbidden', async () => {
|
||||||
|
const error = {
|
||||||
|
message: 'Request failed',
|
||||||
|
response: { status: 403, data: { message: 'Forbidden' } }
|
||||||
|
};
|
||||||
|
await mockAxiosInstance.simulateError('put', error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.transferWorkflow('123', 'project-456');
|
||||||
|
expect.fail('Should have thrown an error');
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).toBeInstanceOf(N8nApiError);
|
||||||
|
expect((err as N8nApiError).statusCode).toBe(403);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createDataTable', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
client = new N8nApiClient(defaultConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create data table with name and columns', async () => {
|
||||||
|
const params = {
|
||||||
|
name: 'My Table',
|
||||||
|
columns: [
|
||||||
|
{ name: 'email', type: 'string' as const },
|
||||||
|
{ name: 'count', type: 'number' as const },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const createdTable = { id: 'dt-1', name: 'My Table', columns: [] };
|
||||||
|
|
||||||
|
mockAxiosInstance.post.mockResolvedValue({ data: createdTable });
|
||||||
|
|
||||||
|
const result = await client.createDataTable(params);
|
||||||
|
|
||||||
|
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/data-tables', params);
|
||||||
|
expect(result).toEqual(createdTable);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create data table without columns', async () => {
|
||||||
|
const params = { name: 'Empty Table' };
|
||||||
|
const createdTable = { id: 'dt-2', name: 'Empty Table' };
|
||||||
|
|
||||||
|
mockAxiosInstance.post.mockResolvedValue({ data: createdTable });
|
||||||
|
|
||||||
|
const result = await client.createDataTable(params);
|
||||||
|
|
||||||
|
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/data-tables', params);
|
||||||
|
expect(result).toEqual(createdTable);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle 400 error', async () => {
|
||||||
|
const error = {
|
||||||
|
message: 'Request failed',
|
||||||
|
response: { status: 400, data: { message: 'Invalid table name' } },
|
||||||
|
};
|
||||||
|
await mockAxiosInstance.simulateError('post', error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.createDataTable({ name: '' });
|
||||||
|
expect.fail('Should have thrown an error');
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).toBeInstanceOf(N8nValidationError);
|
||||||
|
expect((err as N8nValidationError).message).toBe('Invalid table name');
|
||||||
|
expect((err as N8nValidationError).statusCode).toBe(400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('listDataTables', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
client = new N8nApiClient(defaultConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should list data tables successfully', async () => {
|
||||||
|
const response = { data: [{ id: 'dt-1', name: 'Table One' }], nextCursor: 'abc' };
|
||||||
|
mockAxiosInstance.get.mockResolvedValue({ data: response });
|
||||||
|
|
||||||
|
const result = await client.listDataTables({ limit: 10, cursor: 'xyz' });
|
||||||
|
|
||||||
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/data-tables', { params: { limit: 10, cursor: 'xyz' } });
|
||||||
|
expect(result).toEqual(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error', async () => {
|
||||||
|
const error = {
|
||||||
|
message: 'Request failed',
|
||||||
|
response: { status: 500, data: { message: 'Internal server error' } },
|
||||||
|
};
|
||||||
|
await mockAxiosInstance.simulateError('get', error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.listDataTables();
|
||||||
|
expect.fail('Should have thrown an error');
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).toBeInstanceOf(N8nServerError);
|
||||||
|
expect((err as N8nServerError).statusCode).toBe(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getDataTable', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
client = new N8nApiClient(defaultConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get data table successfully', async () => {
|
||||||
|
const table = { id: 'dt-1', name: 'My Table', columns: [] };
|
||||||
|
mockAxiosInstance.get.mockResolvedValue({ data: table });
|
||||||
|
|
||||||
|
const result = await client.getDataTable('dt-1');
|
||||||
|
|
||||||
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/data-tables/dt-1');
|
||||||
|
expect(result).toEqual(table);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle 404 error', async () => {
|
||||||
|
const error = {
|
||||||
|
message: 'Request failed',
|
||||||
|
response: { status: 404, data: { message: 'Data table not found' } },
|
||||||
|
};
|
||||||
|
await mockAxiosInstance.simulateError('get', error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.getDataTable('dt-nonexistent');
|
||||||
|
expect.fail('Should have thrown an error');
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).toBeInstanceOf(N8nNotFoundError);
|
||||||
|
expect((err as N8nNotFoundError).statusCode).toBe(404);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateDataTable', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
client = new N8nApiClient(defaultConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update data table successfully', async () => {
|
||||||
|
const updated = { id: 'dt-1', name: 'Renamed' };
|
||||||
|
mockAxiosInstance.patch.mockResolvedValue({ data: updated });
|
||||||
|
|
||||||
|
const result = await client.updateDataTable('dt-1', { name: 'Renamed' });
|
||||||
|
|
||||||
|
expect(mockAxiosInstance.patch).toHaveBeenCalledWith('/data-tables/dt-1', { name: 'Renamed' });
|
||||||
|
expect(result).toEqual(updated);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error', async () => {
|
||||||
|
const error = {
|
||||||
|
message: 'Request failed',
|
||||||
|
response: { status: 400, data: { message: 'Invalid name' } },
|
||||||
|
};
|
||||||
|
await mockAxiosInstance.simulateError('patch', error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.updateDataTable('dt-1', { name: '' });
|
||||||
|
expect.fail('Should have thrown an error');
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).toBeInstanceOf(N8nValidationError);
|
||||||
|
expect((err as N8nValidationError).statusCode).toBe(400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteDataTable', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
client = new N8nApiClient(defaultConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete data table successfully', async () => {
|
||||||
|
mockAxiosInstance.delete.mockResolvedValue({ data: {} });
|
||||||
|
|
||||||
|
await client.deleteDataTable('dt-1');
|
||||||
|
|
||||||
|
expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/data-tables/dt-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle 404 error', async () => {
|
||||||
|
const error = {
|
||||||
|
message: 'Request failed',
|
||||||
|
response: { status: 404, data: { message: 'Data table not found' } },
|
||||||
|
};
|
||||||
|
await mockAxiosInstance.simulateError('delete', error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.deleteDataTable('dt-nonexistent');
|
||||||
|
expect.fail('Should have thrown an error');
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).toBeInstanceOf(N8nNotFoundError);
|
||||||
|
expect((err as N8nNotFoundError).statusCode).toBe(404);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getDataTableRows', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
client = new N8nApiClient(defaultConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get data table rows with params', async () => {
|
||||||
|
const response = { data: [{ id: 1, email: 'a@b.com' }], nextCursor: null };
|
||||||
|
mockAxiosInstance.get.mockResolvedValue({ data: response });
|
||||||
|
|
||||||
|
const params = { limit: 50, sortBy: 'email:asc', search: 'john' };
|
||||||
|
const result = await client.getDataTableRows('dt-1', params);
|
||||||
|
|
||||||
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/data-tables/dt-1/rows', { params });
|
||||||
|
expect(result).toEqual(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error', async () => {
|
||||||
|
const error = {
|
||||||
|
message: 'Request failed',
|
||||||
|
response: { status: 500, data: { message: 'Internal server error' } },
|
||||||
|
};
|
||||||
|
await mockAxiosInstance.simulateError('get', error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.getDataTableRows('dt-1');
|
||||||
|
expect.fail('Should have thrown an error');
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).toBeInstanceOf(N8nServerError);
|
||||||
|
expect((err as N8nServerError).statusCode).toBe(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('insertDataTableRows', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
client = new N8nApiClient(defaultConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should insert data table rows successfully', async () => {
|
||||||
|
const insertResult = { insertedCount: 2 };
|
||||||
|
mockAxiosInstance.post.mockResolvedValue({ data: insertResult });
|
||||||
|
|
||||||
|
const params = { data: [{ email: 'a@b.com' }, { email: 'c@d.com' }], returnType: 'count' as const };
|
||||||
|
const result = await client.insertDataTableRows('dt-1', params);
|
||||||
|
|
||||||
|
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/data-tables/dt-1/rows', params);
|
||||||
|
expect(result).toEqual(insertResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle 400 error', async () => {
|
||||||
|
const error = {
|
||||||
|
message: 'Request failed',
|
||||||
|
response: { status: 400, data: { message: 'Invalid row data' } },
|
||||||
|
};
|
||||||
|
await mockAxiosInstance.simulateError('post', error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.insertDataTableRows('dt-1', { data: [{}] });
|
||||||
|
expect.fail('Should have thrown an error');
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).toBeInstanceOf(N8nValidationError);
|
||||||
|
expect((err as N8nValidationError).message).toBe('Invalid row data');
|
||||||
|
expect((err as N8nValidationError).statusCode).toBe(400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateDataTableRows', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
client = new N8nApiClient(defaultConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update data table rows successfully', async () => {
|
||||||
|
const updateResult = { updatedCount: 3 };
|
||||||
|
mockAxiosInstance.patch.mockResolvedValue({ data: updateResult });
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
filter: { type: 'and' as const, filters: [{ columnName: 'status', condition: 'eq' as const, value: 'old' }] },
|
||||||
|
data: { status: 'new' },
|
||||||
|
};
|
||||||
|
const result = await client.updateDataTableRows('dt-1', params);
|
||||||
|
|
||||||
|
expect(mockAxiosInstance.patch).toHaveBeenCalledWith('/data-tables/dt-1/rows/update', params);
|
||||||
|
expect(result).toEqual(updateResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error', async () => {
|
||||||
|
const error = {
|
||||||
|
message: 'Request failed',
|
||||||
|
response: { status: 500, data: { message: 'Internal server error' } },
|
||||||
|
};
|
||||||
|
await mockAxiosInstance.simulateError('patch', error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.updateDataTableRows('dt-1', {
|
||||||
|
filter: { type: 'and', filters: [{ columnName: 'id', condition: 'eq', value: 1 }] },
|
||||||
|
data: { name: 'test' },
|
||||||
|
});
|
||||||
|
expect.fail('Should have thrown an error');
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).toBeInstanceOf(N8nServerError);
|
||||||
|
expect((err as N8nServerError).statusCode).toBe(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('upsertDataTableRow', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
client = new N8nApiClient(defaultConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should upsert data table row successfully', async () => {
|
||||||
|
const upsertResult = { action: 'updated', row: { id: 1, email: 'a@b.com' } };
|
||||||
|
mockAxiosInstance.post.mockResolvedValue({ data: upsertResult });
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
filter: { type: 'and' as const, filters: [{ columnName: 'email', condition: 'eq' as const, value: 'a@b.com' }] },
|
||||||
|
data: { score: 15 },
|
||||||
|
};
|
||||||
|
const result = await client.upsertDataTableRow('dt-1', params);
|
||||||
|
|
||||||
|
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/data-tables/dt-1/rows/upsert', params);
|
||||||
|
expect(result).toEqual(upsertResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error', async () => {
|
||||||
|
const error = {
|
||||||
|
message: 'Request failed',
|
||||||
|
response: { status: 400, data: { message: 'Invalid upsert params' } },
|
||||||
|
};
|
||||||
|
await mockAxiosInstance.simulateError('post', error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.upsertDataTableRow('dt-1', {
|
||||||
|
filter: { type: 'and', filters: [{ columnName: 'id', condition: 'eq', value: 1 }] },
|
||||||
|
data: { name: 'test' },
|
||||||
|
});
|
||||||
|
expect.fail('Should have thrown an error');
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).toBeInstanceOf(N8nValidationError);
|
||||||
|
expect((err as N8nValidationError).statusCode).toBe(400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteDataTableRows', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
client = new N8nApiClient(defaultConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete data table rows successfully', async () => {
|
||||||
|
const deleteResult = { deletedCount: 2 };
|
||||||
|
mockAxiosInstance.delete.mockResolvedValue({ data: deleteResult });
|
||||||
|
|
||||||
|
const params = { filter: '{"type":"and","filters":[]}', dryRun: false };
|
||||||
|
const result = await client.deleteDataTableRows('dt-1', params);
|
||||||
|
|
||||||
|
expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/data-tables/dt-1/rows/delete', { params });
|
||||||
|
expect(result).toEqual(deleteResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error', async () => {
|
||||||
|
const error = {
|
||||||
|
message: 'Request failed',
|
||||||
|
response: { status: 500, data: { message: 'Internal server error' } },
|
||||||
|
};
|
||||||
|
await mockAxiosInstance.simulateError('delete', error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.deleteDataTableRows('dt-1', { filter: '{}' });
|
||||||
|
expect.fail('Should have thrown an error');
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).toBeInstanceOf(N8nServerError);
|
||||||
|
expect((err as N8nServerError).statusCode).toBe(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('interceptors', () => {
|
describe('interceptors', () => {
|
||||||
let requestInterceptor: any;
|
let requestInterceptor: any;
|
||||||
let responseInterceptor: any;
|
let responseInterceptor: any;
|
||||||
@@ -1317,4 +1724,4 @@ describe('N8nApiClient', () => {
|
|||||||
expect(result.message).toBe('Bad request');
|
expect(result.message).toBe('Bad request');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ import {
|
|||||||
AddTagOperation,
|
AddTagOperation,
|
||||||
RemoveTagOperation,
|
RemoveTagOperation,
|
||||||
CleanStaleConnectionsOperation,
|
CleanStaleConnectionsOperation,
|
||||||
ReplaceConnectionsOperation
|
ReplaceConnectionsOperation,
|
||||||
|
TransferWorkflowOperation
|
||||||
} from '@/types/workflow-diff';
|
} from '@/types/workflow-diff';
|
||||||
import { Workflow } from '@/types/n8n-api';
|
import { Workflow } from '@/types/n8n-api';
|
||||||
|
|
||||||
@@ -4989,4 +4990,151 @@ describe('WorkflowDiffEngine', () => {
|
|||||||
expect('nonExistent' in updatedNode).toBe(false);
|
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