@pagelines/n8n-mcp 0.3.1 → 0.3.2
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 +10 -6
- package/dist/index.js +2 -71
- package/dist/n8n-client.d.ts +1 -2
- package/dist/n8n-client.js +0 -6
- package/dist/tools.js +0 -29
- package/dist/types.d.ts +1 -28
- package/dist/types.js +1 -1
- package/dist/validators.d.ts +1 -9
- package/dist/validators.js +0 -85
- package/package.json +2 -2
- package/.github/workflows/ci.yml +0 -38
- package/dist/n8n-client.test.d.ts +0 -1
- package/dist/n8n-client.test.js +0 -382
- package/dist/response-format.test.d.ts +0 -1
- package/dist/response-format.test.js +0 -291
- package/dist/validators.test.d.ts +0 -1
- package/dist/validators.test.js +0 -310
- package/docs/best-practices.md +0 -165
- package/docs/node-config.md +0 -205
- package/plans/ai-guidelines.md +0 -233
- package/plans/architecture.md +0 -220
- package/server.json +0 -66
- package/src/autofix.ts +0 -275
- package/src/expressions.ts +0 -254
- package/src/index.ts +0 -550
- package/src/n8n-client.test.ts +0 -467
- package/src/n8n-client.ts +0 -374
- package/src/response-format.test.ts +0 -355
- package/src/response-format.ts +0 -278
- package/src/tools.ts +0 -489
- package/src/types.ts +0 -183
- package/src/validators.test.ts +0 -374
- package/src/validators.ts +0 -394
- package/src/versions.ts +0 -320
- package/tsconfig.json +0 -15
- package/vitest.config.ts +0 -8
package/dist/n8n-client.test.js
DELETED
|
@@ -1,382 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { N8nClient } from './n8n-client.js';
|
|
3
|
-
import { N8N_WORKFLOW_WRITABLE_FIELDS, pickFields } from './types.js';
|
|
4
|
-
// Mock fetch globally
|
|
5
|
-
const mockFetch = vi.fn();
|
|
6
|
-
global.fetch = mockFetch;
|
|
7
|
-
describe('N8nClient', () => {
|
|
8
|
-
let client;
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
client = new N8nClient({
|
|
11
|
-
apiUrl: 'https://n8n.example.com',
|
|
12
|
-
apiKey: 'test-api-key',
|
|
13
|
-
});
|
|
14
|
-
mockFetch.mockReset();
|
|
15
|
-
});
|
|
16
|
-
describe('constructor', () => {
|
|
17
|
-
it('normalizes URL by removing trailing slash', () => {
|
|
18
|
-
const clientWithSlash = new N8nClient({
|
|
19
|
-
apiUrl: 'https://n8n.example.com/',
|
|
20
|
-
apiKey: 'key',
|
|
21
|
-
});
|
|
22
|
-
// Access private property for testing
|
|
23
|
-
expect(clientWithSlash.baseUrl).toBe('https://n8n.example.com');
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
describe('listWorkflows', () => {
|
|
27
|
-
it('calls correct endpoint', async () => {
|
|
28
|
-
mockFetch.mockResolvedValueOnce({
|
|
29
|
-
ok: true,
|
|
30
|
-
text: async () => JSON.stringify({ data: [] }),
|
|
31
|
-
});
|
|
32
|
-
await client.listWorkflows();
|
|
33
|
-
expect(mockFetch).toHaveBeenCalledWith('https://n8n.example.com/api/v1/workflows', expect.objectContaining({
|
|
34
|
-
method: 'GET',
|
|
35
|
-
headers: expect.objectContaining({
|
|
36
|
-
'X-N8N-API-KEY': 'test-api-key',
|
|
37
|
-
}),
|
|
38
|
-
}));
|
|
39
|
-
});
|
|
40
|
-
it('includes query params when provided', async () => {
|
|
41
|
-
mockFetch.mockResolvedValueOnce({
|
|
42
|
-
ok: true,
|
|
43
|
-
text: async () => JSON.stringify({ data: [] }),
|
|
44
|
-
});
|
|
45
|
-
await client.listWorkflows({ active: true, limit: 10 });
|
|
46
|
-
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('active=true'), expect.any(Object));
|
|
47
|
-
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('limit=10'), expect.any(Object));
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
describe('patchWorkflow', () => {
|
|
51
|
-
const mockWorkflow = {
|
|
52
|
-
id: '1',
|
|
53
|
-
name: 'test_workflow',
|
|
54
|
-
active: false,
|
|
55
|
-
nodes: [
|
|
56
|
-
{
|
|
57
|
-
id: 'node1',
|
|
58
|
-
name: 'existing_node',
|
|
59
|
-
type: 'n8n-nodes-base.set',
|
|
60
|
-
typeVersion: 1,
|
|
61
|
-
position: [0, 0],
|
|
62
|
-
parameters: { param1: 'value1', param2: 'value2' },
|
|
63
|
-
},
|
|
64
|
-
],
|
|
65
|
-
connections: {},
|
|
66
|
-
createdAt: '2024-01-01',
|
|
67
|
-
updatedAt: '2024-01-01',
|
|
68
|
-
};
|
|
69
|
-
beforeEach(() => {
|
|
70
|
-
// Mock GET workflow
|
|
71
|
-
mockFetch.mockResolvedValueOnce({
|
|
72
|
-
ok: true,
|
|
73
|
-
text: async () => JSON.stringify(mockWorkflow),
|
|
74
|
-
});
|
|
75
|
-
// Mock PUT workflow
|
|
76
|
-
mockFetch.mockResolvedValueOnce({
|
|
77
|
-
ok: true,
|
|
78
|
-
text: async () => JSON.stringify(mockWorkflow),
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
it('warns when updateNode would remove parameters', async () => {
|
|
82
|
-
const { warnings } = await client.patchWorkflow('1', [
|
|
83
|
-
{
|
|
84
|
-
type: 'updateNode',
|
|
85
|
-
nodeName: 'existing_node',
|
|
86
|
-
properties: {
|
|
87
|
-
parameters: { newParam: 'newValue' }, // Missing param1, param2
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
]);
|
|
91
|
-
expect(warnings).toContainEqual(expect.stringContaining('remove parameters'));
|
|
92
|
-
expect(warnings).toContainEqual(expect.stringContaining('param1'));
|
|
93
|
-
});
|
|
94
|
-
it('warns when removing non-existent node', async () => {
|
|
95
|
-
const { warnings } = await client.patchWorkflow('1', [
|
|
96
|
-
{
|
|
97
|
-
type: 'removeNode',
|
|
98
|
-
nodeName: 'nonexistent_node',
|
|
99
|
-
},
|
|
100
|
-
]);
|
|
101
|
-
expect(warnings).toContainEqual(expect.stringContaining('not found'));
|
|
102
|
-
});
|
|
103
|
-
it('adds node correctly', async () => {
|
|
104
|
-
mockFetch.mockReset();
|
|
105
|
-
mockFetch.mockResolvedValueOnce({
|
|
106
|
-
ok: true,
|
|
107
|
-
text: async () => JSON.stringify(mockWorkflow),
|
|
108
|
-
});
|
|
109
|
-
const updatedWorkflow = {
|
|
110
|
-
...mockWorkflow,
|
|
111
|
-
nodes: [
|
|
112
|
-
...mockWorkflow.nodes,
|
|
113
|
-
{
|
|
114
|
-
id: 'new-id',
|
|
115
|
-
name: 'new_node',
|
|
116
|
-
type: 'n8n-nodes-base.code',
|
|
117
|
-
typeVersion: 1,
|
|
118
|
-
position: [100, 100],
|
|
119
|
-
parameters: {},
|
|
120
|
-
},
|
|
121
|
-
],
|
|
122
|
-
};
|
|
123
|
-
mockFetch.mockResolvedValueOnce({
|
|
124
|
-
ok: true,
|
|
125
|
-
text: async () => JSON.stringify(updatedWorkflow),
|
|
126
|
-
});
|
|
127
|
-
const { workflow } = await client.patchWorkflow('1', [
|
|
128
|
-
{
|
|
129
|
-
type: 'addNode',
|
|
130
|
-
node: {
|
|
131
|
-
name: 'new_node',
|
|
132
|
-
type: 'n8n-nodes-base.code',
|
|
133
|
-
typeVersion: 1,
|
|
134
|
-
position: [100, 100],
|
|
135
|
-
parameters: {},
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
]);
|
|
139
|
-
// Verify PUT was called with the new node
|
|
140
|
-
const putCall = mockFetch.mock.calls[1];
|
|
141
|
-
const putBody = JSON.parse(putCall[1].body);
|
|
142
|
-
expect(putBody.nodes).toHaveLength(2);
|
|
143
|
-
expect(putBody.nodes[1].name).toBe('new_node');
|
|
144
|
-
});
|
|
145
|
-
it('adds connection correctly', async () => {
|
|
146
|
-
mockFetch.mockReset();
|
|
147
|
-
mockFetch.mockResolvedValueOnce({
|
|
148
|
-
ok: true,
|
|
149
|
-
text: async () => JSON.stringify(mockWorkflow),
|
|
150
|
-
});
|
|
151
|
-
const updatedWorkflow = {
|
|
152
|
-
...mockWorkflow,
|
|
153
|
-
connections: {
|
|
154
|
-
existing_node: {
|
|
155
|
-
main: [[{ node: 'target_node', type: 'main', index: 0 }]],
|
|
156
|
-
},
|
|
157
|
-
},
|
|
158
|
-
};
|
|
159
|
-
mockFetch.mockResolvedValueOnce({
|
|
160
|
-
ok: true,
|
|
161
|
-
text: async () => JSON.stringify(updatedWorkflow),
|
|
162
|
-
});
|
|
163
|
-
await client.patchWorkflow('1', [
|
|
164
|
-
{
|
|
165
|
-
type: 'addConnection',
|
|
166
|
-
from: 'existing_node',
|
|
167
|
-
to: 'target_node',
|
|
168
|
-
},
|
|
169
|
-
]);
|
|
170
|
-
const putCall = mockFetch.mock.calls[1];
|
|
171
|
-
const putBody = JSON.parse(putCall[1].body);
|
|
172
|
-
expect(putBody.connections.existing_node.main[0][0].node).toBe('target_node');
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
describe('error handling', () => {
|
|
176
|
-
it('throws on non-ok response', async () => {
|
|
177
|
-
mockFetch.mockResolvedValueOnce({
|
|
178
|
-
ok: false,
|
|
179
|
-
status: 404,
|
|
180
|
-
text: async () => 'Not found',
|
|
181
|
-
});
|
|
182
|
-
await expect(client.getWorkflow('999')).rejects.toThrow('n8n API error (404)');
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
describe('listNodeTypes', () => {
|
|
186
|
-
it('calls correct endpoint', async () => {
|
|
187
|
-
const mockNodeTypes = [
|
|
188
|
-
{
|
|
189
|
-
name: 'n8n-nodes-base.webhook',
|
|
190
|
-
displayName: 'Webhook',
|
|
191
|
-
description: 'Starts workflow on webhook call',
|
|
192
|
-
group: ['trigger'],
|
|
193
|
-
version: 2,
|
|
194
|
-
},
|
|
195
|
-
{
|
|
196
|
-
name: 'n8n-nodes-base.set',
|
|
197
|
-
displayName: 'Set',
|
|
198
|
-
description: 'Set values',
|
|
199
|
-
group: ['transform'],
|
|
200
|
-
version: 3,
|
|
201
|
-
},
|
|
202
|
-
];
|
|
203
|
-
mockFetch.mockResolvedValueOnce({
|
|
204
|
-
ok: true,
|
|
205
|
-
text: async () => JSON.stringify(mockNodeTypes),
|
|
206
|
-
});
|
|
207
|
-
const result = await client.listNodeTypes();
|
|
208
|
-
expect(mockFetch).toHaveBeenCalledWith('https://n8n.example.com/api/v1/nodes', expect.objectContaining({
|
|
209
|
-
method: 'GET',
|
|
210
|
-
headers: expect.objectContaining({
|
|
211
|
-
'X-N8N-API-KEY': 'test-api-key',
|
|
212
|
-
}),
|
|
213
|
-
}));
|
|
214
|
-
expect(result).toHaveLength(2);
|
|
215
|
-
expect(result[0].name).toBe('n8n-nodes-base.webhook');
|
|
216
|
-
expect(result[1].name).toBe('n8n-nodes-base.set');
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
describe('updateWorkflow', () => {
|
|
220
|
-
it('strips disallowed properties before sending to API', async () => {
|
|
221
|
-
const fullWorkflow = {
|
|
222
|
-
id: '123',
|
|
223
|
-
name: 'test_workflow',
|
|
224
|
-
active: true,
|
|
225
|
-
nodes: [{ id: 'n1', name: 'node1', type: 'test', typeVersion: 1, position: [0, 0], parameters: {} }],
|
|
226
|
-
connections: {},
|
|
227
|
-
settings: { timezone: 'UTC' },
|
|
228
|
-
createdAt: '2024-01-01T00:00:00.000Z',
|
|
229
|
-
updatedAt: '2024-01-02T00:00:00.000Z',
|
|
230
|
-
versionId: 'v1',
|
|
231
|
-
staticData: undefined,
|
|
232
|
-
tags: [{ id: 't1', name: 'tag1' }],
|
|
233
|
-
};
|
|
234
|
-
mockFetch.mockResolvedValueOnce({
|
|
235
|
-
ok: true,
|
|
236
|
-
text: async () => JSON.stringify(fullWorkflow),
|
|
237
|
-
});
|
|
238
|
-
await client.updateWorkflow('123', fullWorkflow);
|
|
239
|
-
// Verify the request body does NOT contain disallowed properties
|
|
240
|
-
const putCall = mockFetch.mock.calls[0];
|
|
241
|
-
const putBody = JSON.parse(putCall[1].body);
|
|
242
|
-
// These should be stripped
|
|
243
|
-
expect(putBody.id).toBeUndefined();
|
|
244
|
-
expect(putBody.createdAt).toBeUndefined();
|
|
245
|
-
expect(putBody.updatedAt).toBeUndefined();
|
|
246
|
-
expect(putBody.active).toBeUndefined();
|
|
247
|
-
expect(putBody.versionId).toBeUndefined();
|
|
248
|
-
// These should be preserved
|
|
249
|
-
expect(putBody.name).toBe('test_workflow');
|
|
250
|
-
expect(putBody.nodes).toHaveLength(1);
|
|
251
|
-
expect(putBody.connections).toEqual({});
|
|
252
|
-
expect(putBody.settings).toEqual({ timezone: 'UTC' });
|
|
253
|
-
expect(putBody.staticData).toBeUndefined();
|
|
254
|
-
expect(putBody.tags).toEqual([{ id: 't1', name: 'tag1' }]);
|
|
255
|
-
});
|
|
256
|
-
it('works with partial workflow (only some fields)', async () => {
|
|
257
|
-
mockFetch.mockResolvedValueOnce({
|
|
258
|
-
ok: true,
|
|
259
|
-
text: async () => JSON.stringify({ id: '123', name: 'updated' }),
|
|
260
|
-
});
|
|
261
|
-
await client.updateWorkflow('123', { name: 'updated', nodes: [] });
|
|
262
|
-
const putCall = mockFetch.mock.calls[0];
|
|
263
|
-
const putBody = JSON.parse(putCall[1].body);
|
|
264
|
-
expect(putBody.name).toBe('updated');
|
|
265
|
-
expect(putBody.nodes).toEqual([]);
|
|
266
|
-
});
|
|
267
|
-
it('handles workflow from formatWorkflow (simulating workflow_format apply)', async () => {
|
|
268
|
-
// This simulates the exact scenario that caused the bug:
|
|
269
|
-
// workflow_format returns a full N8nWorkflow object with id, createdAt, etc.
|
|
270
|
-
const formattedWorkflow = {
|
|
271
|
-
id: 'zbB1fCxWgZXgpjB1',
|
|
272
|
-
name: 'my_workflow',
|
|
273
|
-
active: false,
|
|
274
|
-
nodes: [],
|
|
275
|
-
connections: {},
|
|
276
|
-
createdAt: '2024-01-01T00:00:00.000Z',
|
|
277
|
-
updatedAt: '2024-01-02T00:00:00.000Z',
|
|
278
|
-
};
|
|
279
|
-
mockFetch.mockResolvedValueOnce({
|
|
280
|
-
ok: true,
|
|
281
|
-
text: async () => JSON.stringify(formattedWorkflow),
|
|
282
|
-
});
|
|
283
|
-
// This should NOT throw "must NOT have additional properties"
|
|
284
|
-
await client.updateWorkflow('zbB1fCxWgZXgpjB1', formattedWorkflow);
|
|
285
|
-
const putCall = mockFetch.mock.calls[0];
|
|
286
|
-
const putBody = JSON.parse(putCall[1].body);
|
|
287
|
-
// Only writable fields should be sent (schema-driven from N8N_WORKFLOW_WRITABLE_FIELDS)
|
|
288
|
-
const sentKeys = Object.keys(putBody).sort();
|
|
289
|
-
const expectedKeys = ['connections', 'name', 'nodes']; // Only non-undefined writable fields
|
|
290
|
-
expect(sentKeys).toEqual(expectedKeys);
|
|
291
|
-
// Read-only fields must NOT be in request
|
|
292
|
-
expect(putBody.id).toBeUndefined();
|
|
293
|
-
expect(putBody.createdAt).toBeUndefined();
|
|
294
|
-
expect(putBody.updatedAt).toBeUndefined();
|
|
295
|
-
expect(putBody.active).toBeUndefined();
|
|
296
|
-
});
|
|
297
|
-
it('filters out any unknown properties using schema-driven approach', async () => {
|
|
298
|
-
// Real n8n API returns many properties not in our type definition
|
|
299
|
-
// Schema-driven filtering ensures only N8N_WORKFLOW_WRITABLE_FIELDS are sent
|
|
300
|
-
const realN8nWorkflow = {
|
|
301
|
-
id: '123',
|
|
302
|
-
name: 'test_workflow',
|
|
303
|
-
active: true,
|
|
304
|
-
nodes: [{ id: 'n1', name: 'node1', type: 'test', typeVersion: 1, position: [0, 0], parameters: {} }],
|
|
305
|
-
connections: {},
|
|
306
|
-
settings: { timezone: 'UTC' },
|
|
307
|
-
staticData: { lastId: 5 },
|
|
308
|
-
tags: [{ id: 't1', name: 'production' }],
|
|
309
|
-
createdAt: '2024-01-01T00:00:00.000Z',
|
|
310
|
-
updatedAt: '2024-01-02T00:00:00.000Z',
|
|
311
|
-
versionId: 'v1',
|
|
312
|
-
// Properties that real n8n returns but aren't in writable fields:
|
|
313
|
-
homeProject: { id: 'proj1', type: 'personal', name: 'My Project' },
|
|
314
|
-
sharedWithProjects: [],
|
|
315
|
-
usedCredentials: [{ id: 'cred1', name: 'My API Key', type: 'apiKey' }],
|
|
316
|
-
meta: { instanceId: 'abc123' },
|
|
317
|
-
pinData: {},
|
|
318
|
-
triggerCount: 5,
|
|
319
|
-
unknownFutureField: 'whatever',
|
|
320
|
-
};
|
|
321
|
-
mockFetch.mockResolvedValueOnce({
|
|
322
|
-
ok: true,
|
|
323
|
-
text: async () => JSON.stringify(realN8nWorkflow),
|
|
324
|
-
});
|
|
325
|
-
await client.updateWorkflow('123', realN8nWorkflow);
|
|
326
|
-
const putCall = mockFetch.mock.calls[0];
|
|
327
|
-
const putBody = JSON.parse(putCall[1].body);
|
|
328
|
-
// Request should ONLY contain fields from N8N_WORKFLOW_WRITABLE_FIELDS
|
|
329
|
-
const sentKeys = Object.keys(putBody).sort();
|
|
330
|
-
const allowedKeys = [...N8N_WORKFLOW_WRITABLE_FIELDS].sort();
|
|
331
|
-
// Every sent key must be in the allowed list
|
|
332
|
-
for (const key of sentKeys) {
|
|
333
|
-
expect(allowedKeys).toContain(key);
|
|
334
|
-
}
|
|
335
|
-
// Verify exact expected keys (all writable fields that had values)
|
|
336
|
-
expect(sentKeys).toEqual(['connections', 'name', 'nodes', 'settings', 'staticData', 'tags']);
|
|
337
|
-
});
|
|
338
|
-
});
|
|
339
|
-
});
|
|
340
|
-
// ─────────────────────────────────────────────────────────────
|
|
341
|
-
// Schema utilities (types.ts)
|
|
342
|
-
// ─────────────────────────────────────────────────────────────
|
|
343
|
-
describe('pickFields utility', () => {
|
|
344
|
-
it('picks only specified fields', () => {
|
|
345
|
-
const obj = { a: 1, b: 2, c: 3, d: 4 };
|
|
346
|
-
const result = pickFields(obj, ['a', 'c']);
|
|
347
|
-
expect(result).toEqual({ a: 1, c: 3 });
|
|
348
|
-
expect(Object.keys(result)).toEqual(['a', 'c']);
|
|
349
|
-
});
|
|
350
|
-
it('ignores undefined values', () => {
|
|
351
|
-
const obj = { a: 1, b: undefined, c: 3 };
|
|
352
|
-
const result = pickFields(obj, ['a', 'b', 'c']);
|
|
353
|
-
expect(result).toEqual({ a: 1, c: 3 });
|
|
354
|
-
expect('b' in result).toBe(false);
|
|
355
|
-
});
|
|
356
|
-
it('ignores fields not in object', () => {
|
|
357
|
-
const obj = { a: 1 };
|
|
358
|
-
const result = pickFields(obj, ['a', 'missing']);
|
|
359
|
-
expect(result).toEqual({ a: 1 });
|
|
360
|
-
});
|
|
361
|
-
it('returns empty object for empty fields array', () => {
|
|
362
|
-
const obj = { a: 1, b: 2 };
|
|
363
|
-
const result = pickFields(obj, []);
|
|
364
|
-
expect(result).toEqual({});
|
|
365
|
-
});
|
|
366
|
-
});
|
|
367
|
-
describe('N8N_WORKFLOW_WRITABLE_FIELDS schema', () => {
|
|
368
|
-
it('contains expected writable fields', () => {
|
|
369
|
-
expect(N8N_WORKFLOW_WRITABLE_FIELDS).toContain('name');
|
|
370
|
-
expect(N8N_WORKFLOW_WRITABLE_FIELDS).toContain('nodes');
|
|
371
|
-
expect(N8N_WORKFLOW_WRITABLE_FIELDS).toContain('connections');
|
|
372
|
-
expect(N8N_WORKFLOW_WRITABLE_FIELDS).toContain('settings');
|
|
373
|
-
expect(N8N_WORKFLOW_WRITABLE_FIELDS).toContain('staticData');
|
|
374
|
-
expect(N8N_WORKFLOW_WRITABLE_FIELDS).toContain('tags');
|
|
375
|
-
});
|
|
376
|
-
it('does NOT contain read-only fields', () => {
|
|
377
|
-
const readOnlyFields = ['id', 'active', 'createdAt', 'updatedAt', 'versionId'];
|
|
378
|
-
for (const field of readOnlyFields) {
|
|
379
|
-
expect(N8N_WORKFLOW_WRITABLE_FIELDS).not.toContain(field);
|
|
380
|
-
}
|
|
381
|
-
});
|
|
382
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,291 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|