@pagelines/n8n-mcp 0.1.0
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/.github/workflows/ci.yml +38 -0
- package/README.md +155 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +192 -0
- package/dist/n8n-client.d.ts +54 -0
- package/dist/n8n-client.js +275 -0
- package/dist/n8n-client.test.d.ts +1 -0
- package/dist/n8n-client.test.js +184 -0
- package/dist/tools.d.ts +6 -0
- package/dist/tools.js +260 -0
- package/dist/types.d.ts +132 -0
- package/dist/types.js +5 -0
- package/dist/validators.d.ts +10 -0
- package/dist/validators.js +171 -0
- package/dist/validators.test.d.ts +1 -0
- package/dist/validators.test.js +148 -0
- package/package.json +42 -0
- package/server.json +58 -0
- package/src/index.ts +243 -0
- package/src/n8n-client.test.ts +227 -0
- package/src/n8n-client.ts +361 -0
- package/src/tools.ts +273 -0
- package/src/types.ts +107 -0
- package/src/validators.test.ts +180 -0
- package/src/validators.ts +208 -0
- package/tsconfig.json +15 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { N8nClient } from './n8n-client.js';
|
|
3
|
+
|
|
4
|
+
// Mock fetch globally
|
|
5
|
+
const mockFetch = vi.fn();
|
|
6
|
+
global.fetch = mockFetch;
|
|
7
|
+
|
|
8
|
+
describe('N8nClient', () => {
|
|
9
|
+
let client: N8nClient;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
client = new N8nClient({
|
|
13
|
+
apiUrl: 'https://n8n.example.com',
|
|
14
|
+
apiKey: 'test-api-key',
|
|
15
|
+
});
|
|
16
|
+
mockFetch.mockReset();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('constructor', () => {
|
|
20
|
+
it('normalizes URL by removing trailing slash', () => {
|
|
21
|
+
const clientWithSlash = new N8nClient({
|
|
22
|
+
apiUrl: 'https://n8n.example.com/',
|
|
23
|
+
apiKey: 'key',
|
|
24
|
+
});
|
|
25
|
+
// Access private property for testing
|
|
26
|
+
expect((clientWithSlash as any).baseUrl).toBe('https://n8n.example.com');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('listWorkflows', () => {
|
|
31
|
+
it('calls correct endpoint', async () => {
|
|
32
|
+
mockFetch.mockResolvedValueOnce({
|
|
33
|
+
ok: true,
|
|
34
|
+
text: async () => JSON.stringify({ data: [] }),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await client.listWorkflows();
|
|
38
|
+
|
|
39
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
40
|
+
'https://n8n.example.com/api/v1/workflows',
|
|
41
|
+
expect.objectContaining({
|
|
42
|
+
method: 'GET',
|
|
43
|
+
headers: expect.objectContaining({
|
|
44
|
+
'X-N8N-API-KEY': 'test-api-key',
|
|
45
|
+
}),
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('includes query params when provided', async () => {
|
|
51
|
+
mockFetch.mockResolvedValueOnce({
|
|
52
|
+
ok: true,
|
|
53
|
+
text: async () => JSON.stringify({ data: [] }),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await client.listWorkflows({ active: true, limit: 10 });
|
|
57
|
+
|
|
58
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
59
|
+
expect.stringContaining('active=true'),
|
|
60
|
+
expect.any(Object)
|
|
61
|
+
);
|
|
62
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
63
|
+
expect.stringContaining('limit=10'),
|
|
64
|
+
expect.any(Object)
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('patchWorkflow', () => {
|
|
70
|
+
const mockWorkflow = {
|
|
71
|
+
id: '1',
|
|
72
|
+
name: 'test_workflow',
|
|
73
|
+
active: false,
|
|
74
|
+
nodes: [
|
|
75
|
+
{
|
|
76
|
+
id: 'node1',
|
|
77
|
+
name: 'existing_node',
|
|
78
|
+
type: 'n8n-nodes-base.set',
|
|
79
|
+
typeVersion: 1,
|
|
80
|
+
position: [0, 0] as [number, number],
|
|
81
|
+
parameters: { param1: 'value1', param2: 'value2' },
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
connections: {},
|
|
85
|
+
createdAt: '2024-01-01',
|
|
86
|
+
updatedAt: '2024-01-01',
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
// Mock GET workflow
|
|
91
|
+
mockFetch.mockResolvedValueOnce({
|
|
92
|
+
ok: true,
|
|
93
|
+
text: async () => JSON.stringify(mockWorkflow),
|
|
94
|
+
});
|
|
95
|
+
// Mock PUT workflow
|
|
96
|
+
mockFetch.mockResolvedValueOnce({
|
|
97
|
+
ok: true,
|
|
98
|
+
text: async () => JSON.stringify(mockWorkflow),
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('warns when updateNode would remove parameters', async () => {
|
|
103
|
+
const { warnings } = await client.patchWorkflow('1', [
|
|
104
|
+
{
|
|
105
|
+
type: 'updateNode',
|
|
106
|
+
nodeName: 'existing_node',
|
|
107
|
+
properties: {
|
|
108
|
+
parameters: { newParam: 'newValue' }, // Missing param1, param2
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
expect(warnings).toContainEqual(
|
|
114
|
+
expect.stringContaining('remove parameters')
|
|
115
|
+
);
|
|
116
|
+
expect(warnings).toContainEqual(
|
|
117
|
+
expect.stringContaining('param1')
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('warns when removing non-existent node', async () => {
|
|
122
|
+
const { warnings } = await client.patchWorkflow('1', [
|
|
123
|
+
{
|
|
124
|
+
type: 'removeNode',
|
|
125
|
+
nodeName: 'nonexistent_node',
|
|
126
|
+
},
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
expect(warnings).toContainEqual(
|
|
130
|
+
expect.stringContaining('not found')
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('adds node correctly', async () => {
|
|
135
|
+
mockFetch.mockReset();
|
|
136
|
+
mockFetch.mockResolvedValueOnce({
|
|
137
|
+
ok: true,
|
|
138
|
+
text: async () => JSON.stringify(mockWorkflow),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const updatedWorkflow = {
|
|
142
|
+
...mockWorkflow,
|
|
143
|
+
nodes: [
|
|
144
|
+
...mockWorkflow.nodes,
|
|
145
|
+
{
|
|
146
|
+
id: 'new-id',
|
|
147
|
+
name: 'new_node',
|
|
148
|
+
type: 'n8n-nodes-base.code',
|
|
149
|
+
typeVersion: 1,
|
|
150
|
+
position: [100, 100],
|
|
151
|
+
parameters: {},
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
mockFetch.mockResolvedValueOnce({
|
|
157
|
+
ok: true,
|
|
158
|
+
text: async () => JSON.stringify(updatedWorkflow),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const { workflow } = await client.patchWorkflow('1', [
|
|
162
|
+
{
|
|
163
|
+
type: 'addNode',
|
|
164
|
+
node: {
|
|
165
|
+
name: 'new_node',
|
|
166
|
+
type: 'n8n-nodes-base.code',
|
|
167
|
+
typeVersion: 1,
|
|
168
|
+
position: [100, 100],
|
|
169
|
+
parameters: {},
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
// Verify PUT was called with the new node
|
|
175
|
+
const putCall = mockFetch.mock.calls[1];
|
|
176
|
+
const putBody = JSON.parse(putCall[1].body);
|
|
177
|
+
expect(putBody.nodes).toHaveLength(2);
|
|
178
|
+
expect(putBody.nodes[1].name).toBe('new_node');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('adds connection correctly', async () => {
|
|
182
|
+
mockFetch.mockReset();
|
|
183
|
+
mockFetch.mockResolvedValueOnce({
|
|
184
|
+
ok: true,
|
|
185
|
+
text: async () => JSON.stringify(mockWorkflow),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const updatedWorkflow = {
|
|
189
|
+
...mockWorkflow,
|
|
190
|
+
connections: {
|
|
191
|
+
existing_node: {
|
|
192
|
+
main: [[{ node: 'target_node', type: 'main', index: 0 }]],
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
mockFetch.mockResolvedValueOnce({
|
|
198
|
+
ok: true,
|
|
199
|
+
text: async () => JSON.stringify(updatedWorkflow),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await client.patchWorkflow('1', [
|
|
203
|
+
{
|
|
204
|
+
type: 'addConnection',
|
|
205
|
+
from: 'existing_node',
|
|
206
|
+
to: 'target_node',
|
|
207
|
+
},
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
const putCall = mockFetch.mock.calls[1];
|
|
211
|
+
const putBody = JSON.parse(putCall[1].body);
|
|
212
|
+
expect(putBody.connections.existing_node.main[0][0].node).toBe('target_node');
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('error handling', () => {
|
|
217
|
+
it('throws on non-ok response', async () => {
|
|
218
|
+
mockFetch.mockResolvedValueOnce({
|
|
219
|
+
ok: false,
|
|
220
|
+
status: 404,
|
|
221
|
+
text: async () => 'Not found',
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await expect(client.getWorkflow('999')).rejects.toThrow('n8n API error (404)');
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
});
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* n8n REST API Client
|
|
3
|
+
* Clean, minimal implementation with built-in safety checks
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
N8nWorkflow,
|
|
8
|
+
N8nWorkflowListItem,
|
|
9
|
+
N8nExecution,
|
|
10
|
+
N8nExecutionListItem,
|
|
11
|
+
N8nListResponse,
|
|
12
|
+
N8nNode,
|
|
13
|
+
PatchOperation,
|
|
14
|
+
} from './types.js';
|
|
15
|
+
|
|
16
|
+
export interface N8nClientConfig {
|
|
17
|
+
apiUrl: string;
|
|
18
|
+
apiKey: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class N8nClient {
|
|
22
|
+
private baseUrl: string;
|
|
23
|
+
private headers: Record<string, string>;
|
|
24
|
+
|
|
25
|
+
constructor(config: N8nClientConfig) {
|
|
26
|
+
// Normalize URL (remove trailing slash)
|
|
27
|
+
this.baseUrl = config.apiUrl.replace(/\/$/, '');
|
|
28
|
+
this.headers = {
|
|
29
|
+
'X-N8N-API-KEY': config.apiKey,
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─────────────────────────────────────────────────────────────
|
|
35
|
+
// HTTP helpers
|
|
36
|
+
// ─────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
private async request<T>(
|
|
39
|
+
method: string,
|
|
40
|
+
path: string,
|
|
41
|
+
body?: unknown
|
|
42
|
+
): Promise<T> {
|
|
43
|
+
const url = `${this.baseUrl}${path}`;
|
|
44
|
+
|
|
45
|
+
const response = await fetch(url, {
|
|
46
|
+
method,
|
|
47
|
+
headers: this.headers,
|
|
48
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
const text = await response.text();
|
|
53
|
+
throw new Error(`n8n API error (${response.status}): ${text}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Handle empty responses
|
|
57
|
+
const text = await response.text();
|
|
58
|
+
if (!text) return {} as T;
|
|
59
|
+
|
|
60
|
+
return JSON.parse(text) as T;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─────────────────────────────────────────────────────────────
|
|
64
|
+
// Workflow operations
|
|
65
|
+
// ─────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
async listWorkflows(options?: {
|
|
68
|
+
limit?: number;
|
|
69
|
+
cursor?: string;
|
|
70
|
+
active?: boolean;
|
|
71
|
+
tags?: string[];
|
|
72
|
+
}): Promise<N8nListResponse<N8nWorkflowListItem>> {
|
|
73
|
+
const params = new URLSearchParams();
|
|
74
|
+
if (options?.limit) params.set('limit', String(options.limit));
|
|
75
|
+
if (options?.cursor) params.set('cursor', options.cursor);
|
|
76
|
+
if (options?.active !== undefined) params.set('active', String(options.active));
|
|
77
|
+
if (options?.tags?.length) params.set('tags', options.tags.join(','));
|
|
78
|
+
|
|
79
|
+
const query = params.toString();
|
|
80
|
+
return this.request('GET', `/api/v1/workflows${query ? `?${query}` : ''}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async getWorkflow(id: string): Promise<N8nWorkflow> {
|
|
84
|
+
return this.request('GET', `/api/v1/workflows/${id}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async createWorkflow(workflow: {
|
|
88
|
+
name: string;
|
|
89
|
+
nodes: N8nNode[];
|
|
90
|
+
connections: N8nWorkflow['connections'];
|
|
91
|
+
settings?: Record<string, unknown>;
|
|
92
|
+
}): Promise<N8nWorkflow> {
|
|
93
|
+
return this.request('POST', '/api/v1/workflows', workflow);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async updateWorkflow(
|
|
97
|
+
id: string,
|
|
98
|
+
workflow: Partial<Omit<N8nWorkflow, 'id' | 'createdAt' | 'updatedAt'>>
|
|
99
|
+
): Promise<N8nWorkflow> {
|
|
100
|
+
return this.request('PUT', `/api/v1/workflows/${id}`, workflow);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async deleteWorkflow(id: string): Promise<void> {
|
|
104
|
+
await this.request('DELETE', `/api/v1/workflows/${id}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async activateWorkflow(id: string): Promise<N8nWorkflow> {
|
|
108
|
+
return this.request('POST', `/api/v1/workflows/${id}/activate`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async deactivateWorkflow(id: string): Promise<N8nWorkflow> {
|
|
112
|
+
return this.request('POST', `/api/v1/workflows/${id}/deactivate`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─────────────────────────────────────────────────────────────
|
|
116
|
+
// Partial update with safety checks
|
|
117
|
+
// ─────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
async patchWorkflow(
|
|
120
|
+
id: string,
|
|
121
|
+
operations: PatchOperation[]
|
|
122
|
+
): Promise<{ workflow: N8nWorkflow; warnings: string[] }> {
|
|
123
|
+
const warnings: string[] = [];
|
|
124
|
+
|
|
125
|
+
// Fetch current state
|
|
126
|
+
const current = await this.getWorkflow(id);
|
|
127
|
+
|
|
128
|
+
// Apply operations
|
|
129
|
+
const updated = this.applyOperations(current, operations, warnings);
|
|
130
|
+
|
|
131
|
+
// Save
|
|
132
|
+
const result = await this.updateWorkflow(id, {
|
|
133
|
+
name: updated.name,
|
|
134
|
+
nodes: updated.nodes,
|
|
135
|
+
connections: updated.connections,
|
|
136
|
+
settings: updated.settings,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return { workflow: result, warnings };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private applyOperations(
|
|
143
|
+
workflow: N8nWorkflow,
|
|
144
|
+
operations: PatchOperation[],
|
|
145
|
+
warnings: string[]
|
|
146
|
+
): N8nWorkflow {
|
|
147
|
+
// Deep clone
|
|
148
|
+
const result: N8nWorkflow = JSON.parse(JSON.stringify(workflow));
|
|
149
|
+
|
|
150
|
+
for (const op of operations) {
|
|
151
|
+
switch (op.type) {
|
|
152
|
+
case 'addNode': {
|
|
153
|
+
const node: N8nNode = {
|
|
154
|
+
id: op.node.id || crypto.randomUUID(),
|
|
155
|
+
name: op.node.name,
|
|
156
|
+
type: op.node.type,
|
|
157
|
+
typeVersion: op.node.typeVersion,
|
|
158
|
+
position: op.node.position,
|
|
159
|
+
parameters: op.node.parameters,
|
|
160
|
+
...(op.node.credentials && { credentials: op.node.credentials }),
|
|
161
|
+
...(op.node.disabled && { disabled: op.node.disabled }),
|
|
162
|
+
};
|
|
163
|
+
result.nodes.push(node);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
case 'removeNode': {
|
|
168
|
+
const idx = result.nodes.findIndex((n) => n.name === op.nodeName);
|
|
169
|
+
if (idx === -1) {
|
|
170
|
+
warnings.push(`Node not found: ${op.nodeName}`);
|
|
171
|
+
} else {
|
|
172
|
+
result.nodes.splice(idx, 1);
|
|
173
|
+
// Clean up connections
|
|
174
|
+
delete result.connections[op.nodeName];
|
|
175
|
+
for (const [, outputs] of Object.entries(result.connections)) {
|
|
176
|
+
for (const [, connections] of Object.entries(outputs)) {
|
|
177
|
+
for (const connArray of connections) {
|
|
178
|
+
const toRemove = connArray.filter((c) => c.node === op.nodeName);
|
|
179
|
+
for (const conn of toRemove) {
|
|
180
|
+
const i = connArray.indexOf(conn);
|
|
181
|
+
if (i !== -1) connArray.splice(i, 1);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
case 'updateNode': {
|
|
191
|
+
const node = result.nodes.find((n) => n.name === op.nodeName);
|
|
192
|
+
if (!node) {
|
|
193
|
+
warnings.push(`Node not found: ${op.nodeName}`);
|
|
194
|
+
} else {
|
|
195
|
+
// CRITICAL: Warn about parameter replacement
|
|
196
|
+
if (op.properties.parameters) {
|
|
197
|
+
const currentKeys = Object.keys(node.parameters);
|
|
198
|
+
const newKeys = Object.keys(op.properties.parameters);
|
|
199
|
+
const missingKeys = currentKeys.filter((k) => !newKeys.includes(k));
|
|
200
|
+
if (missingKeys.length > 0) {
|
|
201
|
+
warnings.push(
|
|
202
|
+
`WARNING: Updating "${op.nodeName}" will remove parameters: ${missingKeys.join(', ')}. ` +
|
|
203
|
+
`Include all existing parameters to preserve them.`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
Object.assign(node, op.properties);
|
|
208
|
+
}
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
case 'addConnection': {
|
|
213
|
+
const outputType = op.outputType || 'main';
|
|
214
|
+
const fromOutput = op.fromOutput || 0;
|
|
215
|
+
const toInput = op.toInput || 0;
|
|
216
|
+
const inputType = op.inputType || 'main';
|
|
217
|
+
|
|
218
|
+
if (!result.connections[op.from]) {
|
|
219
|
+
result.connections[op.from] = {};
|
|
220
|
+
}
|
|
221
|
+
if (!result.connections[op.from][outputType]) {
|
|
222
|
+
result.connections[op.from][outputType] = [];
|
|
223
|
+
}
|
|
224
|
+
while (result.connections[op.from][outputType].length <= fromOutput) {
|
|
225
|
+
result.connections[op.from][outputType].push([]);
|
|
226
|
+
}
|
|
227
|
+
result.connections[op.from][outputType][fromOutput].push({
|
|
228
|
+
node: op.to,
|
|
229
|
+
type: inputType,
|
|
230
|
+
index: toInput,
|
|
231
|
+
});
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
case 'removeConnection': {
|
|
236
|
+
const outputType = op.outputType || 'main';
|
|
237
|
+
const fromOutput = op.fromOutput || 0;
|
|
238
|
+
const conns = result.connections[op.from]?.[outputType]?.[fromOutput];
|
|
239
|
+
if (conns) {
|
|
240
|
+
const idx = conns.findIndex((c) => c.node === op.to);
|
|
241
|
+
if (idx !== -1) conns.splice(idx, 1);
|
|
242
|
+
}
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
case 'updateSettings': {
|
|
247
|
+
result.settings = { ...result.settings, ...op.settings };
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
case 'updateName': {
|
|
252
|
+
result.name = op.name;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
case 'activate': {
|
|
257
|
+
result.active = true;
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
case 'deactivate': {
|
|
262
|
+
result.active = false;
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ─────────────────────────────────────────────────────────────
|
|
272
|
+
// Execution operations
|
|
273
|
+
// ─────────────────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
async listExecutions(options?: {
|
|
276
|
+
workflowId?: string;
|
|
277
|
+
status?: 'success' | 'error' | 'waiting';
|
|
278
|
+
limit?: number;
|
|
279
|
+
cursor?: string;
|
|
280
|
+
}): Promise<N8nListResponse<N8nExecutionListItem>> {
|
|
281
|
+
const params = new URLSearchParams();
|
|
282
|
+
if (options?.workflowId) params.set('workflowId', options.workflowId);
|
|
283
|
+
if (options?.status) params.set('status', options.status);
|
|
284
|
+
if (options?.limit) params.set('limit', String(options.limit));
|
|
285
|
+
if (options?.cursor) params.set('cursor', options.cursor);
|
|
286
|
+
|
|
287
|
+
const query = params.toString();
|
|
288
|
+
return this.request('GET', `/api/v1/executions${query ? `?${query}` : ''}`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async getExecution(id: string): Promise<N8nExecution> {
|
|
292
|
+
return this.request('GET', `/api/v1/executions/${id}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async deleteExecution(id: string): Promise<void> {
|
|
296
|
+
await this.request('DELETE', `/api/v1/executions/${id}`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ─────────────────────────────────────────────────────────────
|
|
300
|
+
// Workflow execution (webhook trigger)
|
|
301
|
+
// ─────────────────────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
async executeWorkflow(
|
|
304
|
+
id: string,
|
|
305
|
+
data?: Record<string, unknown>
|
|
306
|
+
): Promise<{ executionId?: string; data?: unknown }> {
|
|
307
|
+
// Get workflow to find webhook path
|
|
308
|
+
const workflow = await this.getWorkflow(id);
|
|
309
|
+
|
|
310
|
+
// Find webhook trigger node
|
|
311
|
+
const webhookNode = workflow.nodes.find(
|
|
312
|
+
(n) => n.type === 'n8n-nodes-base.webhook'
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
if (!webhookNode) {
|
|
316
|
+
throw new Error('Workflow has no webhook trigger. Cannot execute via API.');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const path = webhookNode.parameters.path as string;
|
|
320
|
+
if (!path) {
|
|
321
|
+
throw new Error('Webhook node has no path configured.');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Execute via webhook
|
|
325
|
+
const webhookUrl = `${this.baseUrl}/webhook/${path}`;
|
|
326
|
+
const response = await fetch(webhookUrl, {
|
|
327
|
+
method: 'POST',
|
|
328
|
+
headers: { 'Content-Type': 'application/json' },
|
|
329
|
+
body: data ? JSON.stringify(data) : undefined,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
if (!response.ok) {
|
|
333
|
+
const text = await response.text();
|
|
334
|
+
throw new Error(`Webhook execution failed (${response.status}): ${text}`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const result = await response.text();
|
|
338
|
+
try {
|
|
339
|
+
return JSON.parse(result);
|
|
340
|
+
} catch {
|
|
341
|
+
return { data: result };
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ─────────────────────────────────────────────────────────────
|
|
346
|
+
// Health check
|
|
347
|
+
// ─────────────────────────────────────────────────────────────
|
|
348
|
+
|
|
349
|
+
async healthCheck(): Promise<{ healthy: boolean; version?: string; error?: string }> {
|
|
350
|
+
try {
|
|
351
|
+
// Try to list workflows with limit 1
|
|
352
|
+
await this.listWorkflows({ limit: 1 });
|
|
353
|
+
return { healthy: true };
|
|
354
|
+
} catch (error) {
|
|
355
|
+
return {
|
|
356
|
+
healthy: false,
|
|
357
|
+
error: error instanceof Error ? error.message : String(error),
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|