@pagelines/n8n-mcp 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/README.md +40 -24
- package/dist/index.js +122 -18
- package/dist/n8n-client.d.ts +3 -2
- package/dist/n8n-client.js +10 -1
- package/dist/n8n-client.test.js +198 -0
- package/dist/response-format.d.ts +84 -0
- package/dist/response-format.js +183 -0
- package/dist/response-format.test.d.ts +1 -0
- package/dist/response-format.test.js +291 -0
- package/dist/tools.js +67 -3
- package/dist/types.d.ts +44 -0
- package/dist/types.js +34 -1
- package/dist/validators.d.ts +9 -1
- package/dist/validators.js +87 -2
- package/dist/validators.test.js +83 -4
- package/docs/best-practices.md +15 -10
- package/docs/node-config.md +3 -1
- package/logo.png +0 -0
- package/package.json +1 -1
- package/plans/architecture.md +69 -26
- package/src/index.ts +159 -20
- package/src/n8n-client.test.ts +240 -0
- package/src/n8n-client.ts +23 -10
- package/src/response-format.test.ts +355 -0
- package/src/response-format.ts +278 -0
- package/src/tools.ts +68 -3
- package/src/types.ts +76 -0
- package/src/validators.test.ts +101 -4
- package/src/validators.ts +112 -3
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response Format Transformers
|
|
3
|
+
* Reduces response size for MCP to prevent context overflow
|
|
4
|
+
*/
|
|
5
|
+
import type { N8nWorkflow, N8nExecution, N8nExecutionListItem } from './types.js';
|
|
6
|
+
export type ResponseFormat = 'full' | 'compact' | 'summary';
|
|
7
|
+
export interface WorkflowSummary {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
active: boolean;
|
|
11
|
+
nodeCount: number;
|
|
12
|
+
connectionCount: number;
|
|
13
|
+
updatedAt: string;
|
|
14
|
+
nodeTypes: string[];
|
|
15
|
+
}
|
|
16
|
+
export interface WorkflowCompact {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
active: boolean;
|
|
20
|
+
updatedAt: string;
|
|
21
|
+
nodes: CompactNode[];
|
|
22
|
+
connections: Record<string, string[]>;
|
|
23
|
+
settings?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
export interface CompactNode {
|
|
26
|
+
name: string;
|
|
27
|
+
type: string;
|
|
28
|
+
position: [number, number];
|
|
29
|
+
hasCredentials: boolean;
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Format a workflow based on the requested format level
|
|
34
|
+
*/
|
|
35
|
+
export declare function formatWorkflowResponse(workflow: N8nWorkflow, format?: ResponseFormat): N8nWorkflow | WorkflowCompact | WorkflowSummary;
|
|
36
|
+
export interface ExecutionSummary {
|
|
37
|
+
id: string;
|
|
38
|
+
workflowId: string;
|
|
39
|
+
status: string;
|
|
40
|
+
mode: string;
|
|
41
|
+
startedAt: string;
|
|
42
|
+
stoppedAt?: string;
|
|
43
|
+
durationMs?: number;
|
|
44
|
+
hasError: boolean;
|
|
45
|
+
errorMessage?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface ExecutionCompact {
|
|
48
|
+
id: string;
|
|
49
|
+
workflowId: string;
|
|
50
|
+
status: string;
|
|
51
|
+
mode: string;
|
|
52
|
+
startedAt: string;
|
|
53
|
+
stoppedAt?: string;
|
|
54
|
+
finished: boolean;
|
|
55
|
+
error?: {
|
|
56
|
+
message: string;
|
|
57
|
+
};
|
|
58
|
+
nodeResults?: NodeResultSummary[];
|
|
59
|
+
}
|
|
60
|
+
export interface NodeResultSummary {
|
|
61
|
+
nodeName: string;
|
|
62
|
+
itemCount: number;
|
|
63
|
+
success: boolean;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Format an execution based on the requested format level
|
|
67
|
+
*/
|
|
68
|
+
export declare function formatExecutionResponse(execution: N8nExecution, format?: ResponseFormat): N8nExecution | ExecutionCompact | ExecutionSummary;
|
|
69
|
+
/**
|
|
70
|
+
* Format execution list items
|
|
71
|
+
*/
|
|
72
|
+
export declare function formatExecutionListResponse(executions: N8nExecutionListItem[], format?: ResponseFormat): N8nExecutionListItem[] | Array<{
|
|
73
|
+
id: string;
|
|
74
|
+
status: string;
|
|
75
|
+
startedAt: string;
|
|
76
|
+
}>;
|
|
77
|
+
/**
|
|
78
|
+
* Remove null/undefined values and empty objects to reduce size
|
|
79
|
+
*/
|
|
80
|
+
export declare function cleanResponse<T>(obj: T): T;
|
|
81
|
+
/**
|
|
82
|
+
* Stringify with optional minification
|
|
83
|
+
*/
|
|
84
|
+
export declare function stringifyResponse(obj: unknown, minify?: boolean): string;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response Format Transformers
|
|
3
|
+
* Reduces response size for MCP to prevent context overflow
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Format a workflow based on the requested format level
|
|
7
|
+
*/
|
|
8
|
+
export function formatWorkflowResponse(workflow, format = 'compact') {
|
|
9
|
+
switch (format) {
|
|
10
|
+
case 'summary':
|
|
11
|
+
return toWorkflowSummary(workflow);
|
|
12
|
+
case 'compact':
|
|
13
|
+
return toWorkflowCompact(workflow);
|
|
14
|
+
case 'full':
|
|
15
|
+
default:
|
|
16
|
+
return workflow;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function toWorkflowSummary(workflow) {
|
|
20
|
+
const connectionCount = Object.values(workflow.connections).reduce((acc, outputs) => {
|
|
21
|
+
return acc + Object.values(outputs).reduce((a, conns) => {
|
|
22
|
+
return a + conns.reduce((b, c) => b + c.length, 0);
|
|
23
|
+
}, 0);
|
|
24
|
+
}, 0);
|
|
25
|
+
return {
|
|
26
|
+
id: workflow.id,
|
|
27
|
+
name: workflow.name,
|
|
28
|
+
active: workflow.active,
|
|
29
|
+
nodeCount: workflow.nodes.length,
|
|
30
|
+
connectionCount,
|
|
31
|
+
updatedAt: workflow.updatedAt,
|
|
32
|
+
nodeTypes: [...new Set(workflow.nodes.map((n) => n.type))],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function toWorkflowCompact(workflow) {
|
|
36
|
+
// Simplify connections to just show flow: nodeName -> [targetNodeNames]
|
|
37
|
+
const simpleConnections = {};
|
|
38
|
+
for (const [nodeName, outputs] of Object.entries(workflow.connections)) {
|
|
39
|
+
const targets = [];
|
|
40
|
+
for (const conns of Object.values(outputs)) {
|
|
41
|
+
for (const connList of conns) {
|
|
42
|
+
for (const conn of connList) {
|
|
43
|
+
if (!targets.includes(conn.node)) {
|
|
44
|
+
targets.push(conn.node);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (targets.length > 0) {
|
|
50
|
+
simpleConnections[nodeName] = targets;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
id: workflow.id,
|
|
55
|
+
name: workflow.name,
|
|
56
|
+
active: workflow.active,
|
|
57
|
+
updatedAt: workflow.updatedAt,
|
|
58
|
+
nodes: workflow.nodes.map(toCompactNode),
|
|
59
|
+
connections: simpleConnections,
|
|
60
|
+
...(workflow.settings && Object.keys(workflow.settings).length > 0 && { settings: workflow.settings }),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function toCompactNode(node) {
|
|
64
|
+
return {
|
|
65
|
+
name: node.name,
|
|
66
|
+
type: node.type,
|
|
67
|
+
position: node.position,
|
|
68
|
+
hasCredentials: !!node.credentials && Object.keys(node.credentials).length > 0,
|
|
69
|
+
...(node.disabled && { disabled: true }),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Format an execution based on the requested format level
|
|
74
|
+
*/
|
|
75
|
+
export function formatExecutionResponse(execution, format = 'compact') {
|
|
76
|
+
switch (format) {
|
|
77
|
+
case 'summary':
|
|
78
|
+
return toExecutionSummary(execution);
|
|
79
|
+
case 'compact':
|
|
80
|
+
return toExecutionCompact(execution);
|
|
81
|
+
case 'full':
|
|
82
|
+
default:
|
|
83
|
+
return execution;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function toExecutionSummary(execution) {
|
|
87
|
+
const durationMs = execution.stoppedAt && execution.startedAt
|
|
88
|
+
? new Date(execution.stoppedAt).getTime() - new Date(execution.startedAt).getTime()
|
|
89
|
+
: undefined;
|
|
90
|
+
return {
|
|
91
|
+
id: execution.id,
|
|
92
|
+
workflowId: execution.workflowId,
|
|
93
|
+
status: execution.status,
|
|
94
|
+
mode: execution.mode,
|
|
95
|
+
startedAt: execution.startedAt,
|
|
96
|
+
stoppedAt: execution.stoppedAt,
|
|
97
|
+
durationMs,
|
|
98
|
+
hasError: execution.status === 'error',
|
|
99
|
+
errorMessage: execution.data?.resultData?.error?.message,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function toExecutionCompact(execution) {
|
|
103
|
+
const nodeResults = [];
|
|
104
|
+
if (execution.data?.resultData?.runData) {
|
|
105
|
+
for (const [nodeName, runs] of Object.entries(execution.data.resultData.runData)) {
|
|
106
|
+
// runData is Record<string, unknown>, so we need to handle it carefully
|
|
107
|
+
const runArray = runs;
|
|
108
|
+
if (Array.isArray(runArray) && runArray.length > 0) {
|
|
109
|
+
const lastRun = runArray[runArray.length - 1];
|
|
110
|
+
const itemCount = lastRun?.data?.main?.[0]?.length ?? 0;
|
|
111
|
+
nodeResults.push({
|
|
112
|
+
nodeName,
|
|
113
|
+
itemCount,
|
|
114
|
+
success: true,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
id: execution.id,
|
|
121
|
+
workflowId: execution.workflowId,
|
|
122
|
+
status: execution.status,
|
|
123
|
+
mode: execution.mode,
|
|
124
|
+
startedAt: execution.startedAt,
|
|
125
|
+
stoppedAt: execution.stoppedAt,
|
|
126
|
+
finished: execution.finished,
|
|
127
|
+
...(execution.data?.resultData?.error && { error: { message: execution.data.resultData.error.message } }),
|
|
128
|
+
...(nodeResults.length > 0 && { nodeResults }),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Format execution list items
|
|
133
|
+
*/
|
|
134
|
+
export function formatExecutionListResponse(executions, format = 'compact') {
|
|
135
|
+
if (format === 'summary') {
|
|
136
|
+
return executions.map((e) => ({
|
|
137
|
+
id: e.id,
|
|
138
|
+
status: e.status,
|
|
139
|
+
startedAt: e.startedAt,
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
// compact and full are the same for list items (already minimal)
|
|
143
|
+
return executions;
|
|
144
|
+
}
|
|
145
|
+
// ─────────────────────────────────────────────────────────────
|
|
146
|
+
// Generic Response Utilities
|
|
147
|
+
// ─────────────────────────────────────────────────────────────
|
|
148
|
+
/**
|
|
149
|
+
* Remove null/undefined values and empty objects to reduce size
|
|
150
|
+
*/
|
|
151
|
+
export function cleanResponse(obj) {
|
|
152
|
+
if (obj === null || obj === undefined) {
|
|
153
|
+
return obj;
|
|
154
|
+
}
|
|
155
|
+
if (Array.isArray(obj)) {
|
|
156
|
+
return obj.map(cleanResponse);
|
|
157
|
+
}
|
|
158
|
+
if (typeof obj === 'object') {
|
|
159
|
+
const cleaned = {};
|
|
160
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
161
|
+
if (value !== null && value !== undefined) {
|
|
162
|
+
const cleanedValue = cleanResponse(value);
|
|
163
|
+
// Skip empty objects and arrays
|
|
164
|
+
if (typeof cleanedValue === 'object' && cleanedValue !== null) {
|
|
165
|
+
if (Array.isArray(cleanedValue) && cleanedValue.length === 0)
|
|
166
|
+
continue;
|
|
167
|
+
if (!Array.isArray(cleanedValue) && Object.keys(cleanedValue).length === 0)
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
cleaned[key] = cleanedValue;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return cleaned;
|
|
174
|
+
}
|
|
175
|
+
return obj;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Stringify with optional minification
|
|
179
|
+
*/
|
|
180
|
+
export function stringifyResponse(obj, minify = true) {
|
|
181
|
+
const cleaned = cleanResponse(obj);
|
|
182
|
+
return minify ? JSON.stringify(cleaned) : JSON.stringify(cleaned, null, 2);
|
|
183
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { formatWorkflowResponse, formatExecutionResponse, formatExecutionListResponse, cleanResponse, stringifyResponse, } from './response-format.js';
|
|
3
|
+
const createWorkflow = (overrides = {}) => ({
|
|
4
|
+
id: '1',
|
|
5
|
+
name: 'test_workflow',
|
|
6
|
+
active: false,
|
|
7
|
+
nodes: [
|
|
8
|
+
{
|
|
9
|
+
id: 'node1',
|
|
10
|
+
name: 'webhook_trigger',
|
|
11
|
+
type: 'n8n-nodes-base.webhook',
|
|
12
|
+
typeVersion: 1,
|
|
13
|
+
position: [0, 0],
|
|
14
|
+
parameters: { path: 'test', httpMethod: 'POST' },
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: 'node2',
|
|
18
|
+
name: 'set_data',
|
|
19
|
+
type: 'n8n-nodes-base.set',
|
|
20
|
+
typeVersion: 1,
|
|
21
|
+
position: [200, 0],
|
|
22
|
+
parameters: { values: { string: [{ name: 'key', value: 'value' }] } },
|
|
23
|
+
credentials: { httpBasicAuth: { id: '1', name: 'My Auth' } },
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
connections: {
|
|
27
|
+
webhook_trigger: {
|
|
28
|
+
main: [[{ node: 'set_data', type: 'main', index: 0 }]],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
32
|
+
updatedAt: '2024-01-02T00:00:00.000Z',
|
|
33
|
+
...overrides,
|
|
34
|
+
});
|
|
35
|
+
const createExecution = (overrides = {}) => ({
|
|
36
|
+
id: 'exec1',
|
|
37
|
+
workflowId: '1',
|
|
38
|
+
finished: true,
|
|
39
|
+
mode: 'manual',
|
|
40
|
+
startedAt: '2024-01-01T00:00:00.000Z',
|
|
41
|
+
stoppedAt: '2024-01-01T00:00:05.000Z',
|
|
42
|
+
status: 'success',
|
|
43
|
+
data: {
|
|
44
|
+
resultData: {
|
|
45
|
+
runData: {
|
|
46
|
+
webhook_trigger: [{ data: { main: [[{ json: { test: 'data' } }]] } }],
|
|
47
|
+
set_data: [{ data: { main: [[{ json: { key: 'value' } }]] } }],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
...overrides,
|
|
52
|
+
});
|
|
53
|
+
describe('formatWorkflowResponse', () => {
|
|
54
|
+
describe('summary format', () => {
|
|
55
|
+
it('returns minimal workflow info', () => {
|
|
56
|
+
const workflow = createWorkflow();
|
|
57
|
+
const result = formatWorkflowResponse(workflow, 'summary');
|
|
58
|
+
expect(result.id).toBe('1');
|
|
59
|
+
expect(result.name).toBe('test_workflow');
|
|
60
|
+
expect(result.active).toBe(false);
|
|
61
|
+
expect(result.nodeCount).toBe(2);
|
|
62
|
+
expect(result.connectionCount).toBe(1);
|
|
63
|
+
expect(result.updatedAt).toBe('2024-01-02T00:00:00.000Z');
|
|
64
|
+
expect(result.nodeTypes).toContain('n8n-nodes-base.webhook');
|
|
65
|
+
expect(result.nodeTypes).toContain('n8n-nodes-base.set');
|
|
66
|
+
// Should not have full nodes or connections
|
|
67
|
+
expect(result.nodes).toBeUndefined();
|
|
68
|
+
expect(result.connections).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe('compact format', () => {
|
|
72
|
+
it('returns nodes without parameters', () => {
|
|
73
|
+
const workflow = createWorkflow();
|
|
74
|
+
const result = formatWorkflowResponse(workflow, 'compact');
|
|
75
|
+
expect(result.id).toBe('1');
|
|
76
|
+
expect(result.name).toBe('test_workflow');
|
|
77
|
+
expect(result.nodes).toHaveLength(2);
|
|
78
|
+
// Nodes should have name, type, position but no parameters
|
|
79
|
+
expect(result.nodes[0].name).toBe('webhook_trigger');
|
|
80
|
+
expect(result.nodes[0].type).toBe('n8n-nodes-base.webhook');
|
|
81
|
+
expect(result.nodes[0].position).toEqual([0, 0]);
|
|
82
|
+
expect(result.nodes[0].hasCredentials).toBe(false);
|
|
83
|
+
expect(result.nodes[0].parameters).toBeUndefined();
|
|
84
|
+
// Second node has credentials
|
|
85
|
+
expect(result.nodes[1].hasCredentials).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
it('simplifies connections to node -> [targets] map', () => {
|
|
88
|
+
const workflow = createWorkflow();
|
|
89
|
+
const result = formatWorkflowResponse(workflow, 'compact');
|
|
90
|
+
expect(result.connections).toEqual({
|
|
91
|
+
webhook_trigger: ['set_data'],
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
it('marks disabled nodes', () => {
|
|
95
|
+
const workflow = createWorkflow({
|
|
96
|
+
nodes: [
|
|
97
|
+
{
|
|
98
|
+
id: 'node1',
|
|
99
|
+
name: 'disabled_node',
|
|
100
|
+
type: 'n8n-nodes-base.set',
|
|
101
|
+
typeVersion: 1,
|
|
102
|
+
position: [0, 0],
|
|
103
|
+
parameters: {},
|
|
104
|
+
disabled: true,
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
});
|
|
108
|
+
const result = formatWorkflowResponse(workflow, 'compact');
|
|
109
|
+
expect(result.nodes[0].disabled).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
describe('full format', () => {
|
|
113
|
+
it('returns complete workflow', () => {
|
|
114
|
+
const workflow = createWorkflow();
|
|
115
|
+
const result = formatWorkflowResponse(workflow, 'full');
|
|
116
|
+
expect(result).toEqual(workflow);
|
|
117
|
+
expect(result.nodes[0].parameters).toEqual({ path: 'test', httpMethod: 'POST' });
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
describe('default format', () => {
|
|
121
|
+
it('defaults to compact', () => {
|
|
122
|
+
const workflow = createWorkflow();
|
|
123
|
+
const result = formatWorkflowResponse(workflow);
|
|
124
|
+
// Should be compact (no parameters)
|
|
125
|
+
expect(result.nodes[0].parameters).toBeUndefined();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
describe('formatExecutionResponse', () => {
|
|
130
|
+
describe('summary format', () => {
|
|
131
|
+
it('returns minimal execution info', () => {
|
|
132
|
+
const execution = createExecution();
|
|
133
|
+
const result = formatExecutionResponse(execution, 'summary');
|
|
134
|
+
expect(result.id).toBe('exec1');
|
|
135
|
+
expect(result.workflowId).toBe('1');
|
|
136
|
+
expect(result.status).toBe('success');
|
|
137
|
+
expect(result.mode).toBe('manual');
|
|
138
|
+
expect(result.durationMs).toBe(5000);
|
|
139
|
+
expect(result.hasError).toBe(false);
|
|
140
|
+
// Should not have runData
|
|
141
|
+
expect(result.data).toBeUndefined();
|
|
142
|
+
});
|
|
143
|
+
it('includes error message when present', () => {
|
|
144
|
+
const execution = createExecution({
|
|
145
|
+
status: 'error',
|
|
146
|
+
data: {
|
|
147
|
+
resultData: {
|
|
148
|
+
error: { message: 'Something went wrong' },
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
const result = formatExecutionResponse(execution, 'summary');
|
|
153
|
+
expect(result.hasError).toBe(true);
|
|
154
|
+
expect(result.errorMessage).toBe('Something went wrong');
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
describe('compact format', () => {
|
|
158
|
+
it('returns execution without runData but with node summaries', () => {
|
|
159
|
+
const execution = createExecution();
|
|
160
|
+
const result = formatExecutionResponse(execution, 'compact');
|
|
161
|
+
expect(result.id).toBe('exec1');
|
|
162
|
+
expect(result.status).toBe('success');
|
|
163
|
+
expect(result.finished).toBe(true);
|
|
164
|
+
// Should have node result summaries
|
|
165
|
+
expect(result.nodeResults).toHaveLength(2);
|
|
166
|
+
expect(result.nodeResults[0].nodeName).toBe('webhook_trigger');
|
|
167
|
+
expect(result.nodeResults[0].itemCount).toBe(1);
|
|
168
|
+
// Should not have full runData
|
|
169
|
+
expect(result.data).toBeUndefined();
|
|
170
|
+
});
|
|
171
|
+
it('includes error in compact format', () => {
|
|
172
|
+
const execution = createExecution({
|
|
173
|
+
status: 'error',
|
|
174
|
+
data: {
|
|
175
|
+
resultData: {
|
|
176
|
+
error: { message: 'Failed' },
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
const result = formatExecutionResponse(execution, 'compact');
|
|
181
|
+
expect(result.error).toEqual({ message: 'Failed' });
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
describe('full format', () => {
|
|
185
|
+
it('returns complete execution with runData', () => {
|
|
186
|
+
const execution = createExecution();
|
|
187
|
+
const result = formatExecutionResponse(execution, 'full');
|
|
188
|
+
expect(result).toEqual(execution);
|
|
189
|
+
expect(result.data?.resultData?.runData).toBeDefined();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
describe('formatExecutionListResponse', () => {
|
|
194
|
+
const executions = [
|
|
195
|
+
{ id: '1', workflowId: 'w1', status: 'success', startedAt: '2024-01-01', mode: 'manual' },
|
|
196
|
+
{ id: '2', workflowId: 'w1', status: 'error', startedAt: '2024-01-02', mode: 'webhook' },
|
|
197
|
+
];
|
|
198
|
+
it('summary returns id, status, startedAt only', () => {
|
|
199
|
+
const result = formatExecutionListResponse(executions, 'summary');
|
|
200
|
+
expect(result).toHaveLength(2);
|
|
201
|
+
expect(result[0]).toEqual({ id: '1', status: 'success', startedAt: '2024-01-01' });
|
|
202
|
+
expect(result[0].workflowId).toBeUndefined();
|
|
203
|
+
expect(result[0].mode).toBeUndefined();
|
|
204
|
+
});
|
|
205
|
+
it('compact and full return same as input', () => {
|
|
206
|
+
const compactResult = formatExecutionListResponse(executions, 'compact');
|
|
207
|
+
const fullResult = formatExecutionListResponse(executions, 'full');
|
|
208
|
+
expect(compactResult).toEqual(executions);
|
|
209
|
+
expect(fullResult).toEqual(executions);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
describe('cleanResponse', () => {
|
|
213
|
+
it('removes null values', () => {
|
|
214
|
+
const obj = { a: 1, b: null, c: 'test' };
|
|
215
|
+
const result = cleanResponse(obj);
|
|
216
|
+
expect(result).toEqual({ a: 1, c: 'test' });
|
|
217
|
+
});
|
|
218
|
+
it('removes undefined values', () => {
|
|
219
|
+
const obj = { a: 1, b: undefined, c: 'test' };
|
|
220
|
+
const result = cleanResponse(obj);
|
|
221
|
+
expect(result).toEqual({ a: 1, c: 'test' });
|
|
222
|
+
});
|
|
223
|
+
it('removes empty objects', () => {
|
|
224
|
+
const obj = { a: 1, b: {}, c: 'test' };
|
|
225
|
+
const result = cleanResponse(obj);
|
|
226
|
+
expect(result).toEqual({ a: 1, c: 'test' });
|
|
227
|
+
});
|
|
228
|
+
it('removes empty arrays', () => {
|
|
229
|
+
const obj = { a: 1, b: [], c: 'test' };
|
|
230
|
+
const result = cleanResponse(obj);
|
|
231
|
+
expect(result).toEqual({ a: 1, c: 'test' });
|
|
232
|
+
});
|
|
233
|
+
it('handles nested objects', () => {
|
|
234
|
+
const obj = { a: { b: null, c: 1 }, d: { e: {} } };
|
|
235
|
+
const result = cleanResponse(obj);
|
|
236
|
+
expect(result).toEqual({ a: { c: 1 } });
|
|
237
|
+
});
|
|
238
|
+
it('handles arrays', () => {
|
|
239
|
+
const arr = [{ a: 1, b: null }, { c: 2 }];
|
|
240
|
+
const result = cleanResponse(arr);
|
|
241
|
+
expect(result).toEqual([{ a: 1 }, { c: 2 }]);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
describe('stringifyResponse', () => {
|
|
245
|
+
it('minifies by default', () => {
|
|
246
|
+
const obj = { a: 1, b: 'test' };
|
|
247
|
+
const result = stringifyResponse(obj);
|
|
248
|
+
expect(result).toBe('{"a":1,"b":"test"}');
|
|
249
|
+
expect(result).not.toContain('\n');
|
|
250
|
+
});
|
|
251
|
+
it('can pretty print when minify=false', () => {
|
|
252
|
+
const obj = { a: 1 };
|
|
253
|
+
const result = stringifyResponse(obj, false);
|
|
254
|
+
expect(result).toContain('\n');
|
|
255
|
+
});
|
|
256
|
+
it('cleans null values before stringifying', () => {
|
|
257
|
+
const obj = { a: 1, b: null };
|
|
258
|
+
const result = stringifyResponse(obj);
|
|
259
|
+
expect(result).toBe('{"a":1}');
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
describe('token reduction estimates', () => {
|
|
263
|
+
it('compact format significantly reduces workflow size', () => {
|
|
264
|
+
const workflow = createWorkflow({
|
|
265
|
+
nodes: Array.from({ length: 10 }, (_, i) => ({
|
|
266
|
+
id: `node${i}`,
|
|
267
|
+
name: `node_${i}`,
|
|
268
|
+
type: 'n8n-nodes-base.set',
|
|
269
|
+
typeVersion: 1,
|
|
270
|
+
position: [i * 200, 0],
|
|
271
|
+
parameters: {
|
|
272
|
+
values: {
|
|
273
|
+
string: Array.from({ length: 10 }, (_, j) => ({
|
|
274
|
+
name: `param_${j}`,
|
|
275
|
+
value: `This is a long value that takes up tokens ${j}`,
|
|
276
|
+
})),
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
})),
|
|
280
|
+
});
|
|
281
|
+
const fullJson = JSON.stringify(workflow);
|
|
282
|
+
const compactJson = stringifyResponse(formatWorkflowResponse(workflow, 'compact'));
|
|
283
|
+
const summaryJson = stringifyResponse(formatWorkflowResponse(workflow, 'summary'));
|
|
284
|
+
// Compact should be significantly smaller than full
|
|
285
|
+
expect(compactJson.length).toBeLessThan(fullJson.length * 0.5);
|
|
286
|
+
// Summary should be smallest
|
|
287
|
+
expect(summaryJson.length).toBeLessThan(compactJson.length);
|
|
288
|
+
console.log(`Full: ${fullJson.length} chars, Compact: ${compactJson.length} chars, Summary: ${summaryJson.length} chars`);
|
|
289
|
+
console.log(`Reduction: ${Math.round((1 - compactJson.length / fullJson.length) * 100)}% (compact), ${Math.round((1 - summaryJson.length / fullJson.length) * 100)}% (summary)`);
|
|
290
|
+
});
|
|
291
|
+
});
|