@pagelines/n8n-mcp 0.3.0 → 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 +12 -0
- package/dist/index.js +1 -1
- package/dist/n8n-client.d.ts +1 -1
- package/dist/n8n-client.js +3 -2
- package/dist/n8n-client.test.js +90 -3
- package/dist/types.d.ts +17 -0
- package/dist/types.js +34 -1
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/n8n-client.test.ts +108 -3
- package/src/n8n-client.ts +13 -11
- package/src/types.ts +43 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.3.1] - 2025-01-13
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **Critical bug**: `workflow_update`, `workflow_format`, and `workflow_autofix` failing with "request/body must NOT have additional properties" error
|
|
9
|
+
- Root cause: n8n API returns additional read-only properties that were being sent back on PUT requests
|
|
10
|
+
- Solution: Schema-driven field filtering using `N8N_WORKFLOW_WRITABLE_FIELDS` allowlist (source of truth: n8n OpenAPI spec at `/api/v1/openapi.yml`)
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Refactored `updateWorkflow` to use schema-driven approach instead of property denylist
|
|
14
|
+
- Added `pickFields` generic utility for type-safe field filtering
|
|
15
|
+
- Added comprehensive tests for schema-driven filtering
|
|
16
|
+
|
|
5
17
|
## [0.3.0] - 2025-01-13
|
|
6
18
|
|
|
7
19
|
### Added
|
package/dist/index.js
CHANGED
package/dist/n8n-client.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* n8n REST API Client
|
|
3
3
|
* Clean, minimal implementation with built-in safety checks
|
|
4
4
|
*/
|
|
5
|
-
import type
|
|
5
|
+
import { type N8nWorkflow, type N8nWorkflowListItem, type N8nExecution, type N8nExecutionListItem, type N8nListResponse, type N8nNode, type N8nNodeType, type PatchOperation } from './types.js';
|
|
6
6
|
export interface N8nClientConfig {
|
|
7
7
|
apiUrl: string;
|
|
8
8
|
apiKey: string;
|
package/dist/n8n-client.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* n8n REST API Client
|
|
3
3
|
* Clean, minimal implementation with built-in safety checks
|
|
4
4
|
*/
|
|
5
|
+
import { N8N_WORKFLOW_WRITABLE_FIELDS, pickFields, } from './types.js';
|
|
5
6
|
export class N8nClient {
|
|
6
7
|
baseUrl;
|
|
7
8
|
headers;
|
|
@@ -56,8 +57,8 @@ export class N8nClient {
|
|
|
56
57
|
return this.request('POST', '/api/v1/workflows', workflow);
|
|
57
58
|
}
|
|
58
59
|
async updateWorkflow(id, workflow) {
|
|
59
|
-
//
|
|
60
|
-
const
|
|
60
|
+
// Schema-driven: only send fields n8n accepts (defined in types.ts)
|
|
61
|
+
const allowed = pickFields(workflow, N8N_WORKFLOW_WRITABLE_FIELDS);
|
|
61
62
|
return this.request('PUT', `/api/v1/workflows/${id}`, allowed);
|
|
62
63
|
}
|
|
63
64
|
async deleteWorkflow(id) {
|
package/dist/n8n-client.test.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import { N8nClient } from './n8n-client.js';
|
|
3
|
+
import { N8N_WORKFLOW_WRITABLE_FIELDS, pickFields } from './types.js';
|
|
3
4
|
// Mock fetch globally
|
|
4
5
|
const mockFetch = vi.fn();
|
|
5
6
|
global.fetch = mockFetch;
|
|
@@ -283,13 +284,99 @@ describe('N8nClient', () => {
|
|
|
283
284
|
await client.updateWorkflow('zbB1fCxWgZXgpjB1', formattedWorkflow);
|
|
284
285
|
const putCall = mockFetch.mock.calls[0];
|
|
285
286
|
const putBody = JSON.parse(putCall[1].body);
|
|
286
|
-
//
|
|
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
|
|
287
292
|
expect(putBody.id).toBeUndefined();
|
|
288
293
|
expect(putBody.createdAt).toBeUndefined();
|
|
289
294
|
expect(putBody.updatedAt).toBeUndefined();
|
|
290
295
|
expect(putBody.active).toBeUndefined();
|
|
291
|
-
// Only allowed properties should be sent
|
|
292
|
-
expect(Object.keys(putBody).sort()).toEqual(['connections', 'name', 'nodes']);
|
|
293
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
|
+
}
|
|
294
381
|
});
|
|
295
382
|
});
|
package/dist/types.d.ts
CHANGED
|
@@ -130,6 +130,23 @@ export interface N8nListResponse<T> {
|
|
|
130
130
|
data: T[];
|
|
131
131
|
nextCursor?: string;
|
|
132
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Fields that n8n API accepts on PUT /workflows/:id
|
|
135
|
+
* All other fields (id, createdAt, homeProject, etc.) are read-only
|
|
136
|
+
*
|
|
137
|
+
* Derived from: n8n OpenAPI spec (GET {n8n-url}/api/v1/openapi.yml)
|
|
138
|
+
* Path: /workflows/{id} → put → requestBody → content → application/json → schema
|
|
139
|
+
*
|
|
140
|
+
* If n8n adds new writable fields, check the OpenAPI spec and update this array.
|
|
141
|
+
*/
|
|
142
|
+
export declare const N8N_WORKFLOW_WRITABLE_FIELDS: readonly ["name", "nodes", "connections", "settings", "staticData", "tags"];
|
|
143
|
+
export type N8nWorkflowWritableField = (typeof N8N_WORKFLOW_WRITABLE_FIELDS)[number];
|
|
144
|
+
export type N8nWorkflowUpdate = Pick<N8nWorkflow, N8nWorkflowWritableField>;
|
|
145
|
+
/**
|
|
146
|
+
* Pick only specified fields from an object (strips everything else)
|
|
147
|
+
* Generic utility for schema-driven field filtering
|
|
148
|
+
*/
|
|
149
|
+
export declare function pickFields<T, K extends keyof T>(obj: T, fields: readonly K[]): Pick<T, K>;
|
|
133
150
|
export interface N8nNodeType {
|
|
134
151
|
name: string;
|
|
135
152
|
displayName: string;
|
package/dist/types.js
CHANGED
|
@@ -2,4 +2,37 @@
|
|
|
2
2
|
* n8n API Types
|
|
3
3
|
* Minimal types for workflow management
|
|
4
4
|
*/
|
|
5
|
-
|
|
5
|
+
// ─────────────────────────────────────────────────────────────
|
|
6
|
+
// Schema-driven field definitions
|
|
7
|
+
// Source of truth: n8n OpenAPI spec at /api/v1/openapi.yml
|
|
8
|
+
// ─────────────────────────────────────────────────────────────
|
|
9
|
+
/**
|
|
10
|
+
* Fields that n8n API accepts on PUT /workflows/:id
|
|
11
|
+
* All other fields (id, createdAt, homeProject, etc.) are read-only
|
|
12
|
+
*
|
|
13
|
+
* Derived from: n8n OpenAPI spec (GET {n8n-url}/api/v1/openapi.yml)
|
|
14
|
+
* Path: /workflows/{id} → put → requestBody → content → application/json → schema
|
|
15
|
+
*
|
|
16
|
+
* If n8n adds new writable fields, check the OpenAPI spec and update this array.
|
|
17
|
+
*/
|
|
18
|
+
export const N8N_WORKFLOW_WRITABLE_FIELDS = [
|
|
19
|
+
'name',
|
|
20
|
+
'nodes',
|
|
21
|
+
'connections',
|
|
22
|
+
'settings',
|
|
23
|
+
'staticData',
|
|
24
|
+
'tags',
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* Pick only specified fields from an object (strips everything else)
|
|
28
|
+
* Generic utility for schema-driven field filtering
|
|
29
|
+
*/
|
|
30
|
+
export function pickFields(obj, fields) {
|
|
31
|
+
const result = {};
|
|
32
|
+
for (const field of fields) {
|
|
33
|
+
if (field in obj && obj[field] !== undefined) {
|
|
34
|
+
result[field] = obj[field];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
package/src/n8n-client.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import { N8nClient } from './n8n-client.js';
|
|
3
|
+
import { N8N_WORKFLOW_WRITABLE_FIELDS, pickFields } from './types.js';
|
|
3
4
|
|
|
4
5
|
// Mock fetch globally
|
|
5
6
|
const mockFetch = vi.fn();
|
|
@@ -349,14 +350,118 @@ describe('N8nClient', () => {
|
|
|
349
350
|
const putCall = mockFetch.mock.calls[0];
|
|
350
351
|
const putBody = JSON.parse(putCall[1].body);
|
|
351
352
|
|
|
352
|
-
//
|
|
353
|
+
// Only writable fields should be sent (schema-driven from N8N_WORKFLOW_WRITABLE_FIELDS)
|
|
354
|
+
const sentKeys = Object.keys(putBody).sort();
|
|
355
|
+
const expectedKeys = ['connections', 'name', 'nodes']; // Only non-undefined writable fields
|
|
356
|
+
expect(sentKeys).toEqual(expectedKeys);
|
|
357
|
+
|
|
358
|
+
// Read-only fields must NOT be in request
|
|
353
359
|
expect(putBody.id).toBeUndefined();
|
|
354
360
|
expect(putBody.createdAt).toBeUndefined();
|
|
355
361
|
expect(putBody.updatedAt).toBeUndefined();
|
|
356
362
|
expect(putBody.active).toBeUndefined();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('filters out any unknown properties using schema-driven approach', async () => {
|
|
366
|
+
// Real n8n API returns many properties not in our type definition
|
|
367
|
+
// Schema-driven filtering ensures only N8N_WORKFLOW_WRITABLE_FIELDS are sent
|
|
368
|
+
const realN8nWorkflow = {
|
|
369
|
+
id: '123',
|
|
370
|
+
name: 'test_workflow',
|
|
371
|
+
active: true,
|
|
372
|
+
nodes: [{ id: 'n1', name: 'node1', type: 'test', typeVersion: 1, position: [0, 0] as [number, number], parameters: {} }],
|
|
373
|
+
connections: {},
|
|
374
|
+
settings: { timezone: 'UTC' },
|
|
375
|
+
staticData: { lastId: 5 },
|
|
376
|
+
tags: [{ id: 't1', name: 'production' }],
|
|
377
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
378
|
+
updatedAt: '2024-01-02T00:00:00.000Z',
|
|
379
|
+
versionId: 'v1',
|
|
380
|
+
// Properties that real n8n returns but aren't in writable fields:
|
|
381
|
+
homeProject: { id: 'proj1', type: 'personal', name: 'My Project' },
|
|
382
|
+
sharedWithProjects: [],
|
|
383
|
+
usedCredentials: [{ id: 'cred1', name: 'My API Key', type: 'apiKey' }],
|
|
384
|
+
meta: { instanceId: 'abc123' },
|
|
385
|
+
pinData: {},
|
|
386
|
+
triggerCount: 5,
|
|
387
|
+
unknownFutureField: 'whatever',
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
mockFetch.mockResolvedValueOnce({
|
|
391
|
+
ok: true,
|
|
392
|
+
text: async () => JSON.stringify(realN8nWorkflow),
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
await client.updateWorkflow('123', realN8nWorkflow as any);
|
|
396
|
+
|
|
397
|
+
const putCall = mockFetch.mock.calls[0];
|
|
398
|
+
const putBody = JSON.parse(putCall[1].body);
|
|
357
399
|
|
|
358
|
-
//
|
|
359
|
-
|
|
400
|
+
// Request should ONLY contain fields from N8N_WORKFLOW_WRITABLE_FIELDS
|
|
401
|
+
const sentKeys = Object.keys(putBody).sort();
|
|
402
|
+
const allowedKeys = [...N8N_WORKFLOW_WRITABLE_FIELDS].sort();
|
|
403
|
+
|
|
404
|
+
// Every sent key must be in the allowed list
|
|
405
|
+
for (const key of sentKeys) {
|
|
406
|
+
expect(allowedKeys).toContain(key);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Verify exact expected keys (all writable fields that had values)
|
|
410
|
+
expect(sentKeys).toEqual(['connections', 'name', 'nodes', 'settings', 'staticData', 'tags']);
|
|
360
411
|
});
|
|
361
412
|
});
|
|
362
413
|
});
|
|
414
|
+
|
|
415
|
+
// ─────────────────────────────────────────────────────────────
|
|
416
|
+
// Schema utilities (types.ts)
|
|
417
|
+
// ─────────────────────────────────────────────────────────────
|
|
418
|
+
|
|
419
|
+
describe('pickFields utility', () => {
|
|
420
|
+
it('picks only specified fields', () => {
|
|
421
|
+
const obj = { a: 1, b: 2, c: 3, d: 4 };
|
|
422
|
+
const result = pickFields(obj, ['a', 'c'] as const);
|
|
423
|
+
|
|
424
|
+
expect(result).toEqual({ a: 1, c: 3 });
|
|
425
|
+
expect(Object.keys(result)).toEqual(['a', 'c']);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('ignores undefined values', () => {
|
|
429
|
+
const obj = { a: 1, b: undefined, c: 3 };
|
|
430
|
+
const result = pickFields(obj, ['a', 'b', 'c'] as const);
|
|
431
|
+
|
|
432
|
+
expect(result).toEqual({ a: 1, c: 3 });
|
|
433
|
+
expect('b' in result).toBe(false);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('ignores fields not in object', () => {
|
|
437
|
+
const obj = { a: 1 };
|
|
438
|
+
const result = pickFields(obj as any, ['a', 'missing'] as const);
|
|
439
|
+
|
|
440
|
+
expect(result).toEqual({ a: 1 });
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('returns empty object for empty fields array', () => {
|
|
444
|
+
const obj = { a: 1, b: 2 };
|
|
445
|
+
const result = pickFields(obj, [] as const);
|
|
446
|
+
|
|
447
|
+
expect(result).toEqual({});
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
describe('N8N_WORKFLOW_WRITABLE_FIELDS schema', () => {
|
|
452
|
+
it('contains expected writable fields', () => {
|
|
453
|
+
expect(N8N_WORKFLOW_WRITABLE_FIELDS).toContain('name');
|
|
454
|
+
expect(N8N_WORKFLOW_WRITABLE_FIELDS).toContain('nodes');
|
|
455
|
+
expect(N8N_WORKFLOW_WRITABLE_FIELDS).toContain('connections');
|
|
456
|
+
expect(N8N_WORKFLOW_WRITABLE_FIELDS).toContain('settings');
|
|
457
|
+
expect(N8N_WORKFLOW_WRITABLE_FIELDS).toContain('staticData');
|
|
458
|
+
expect(N8N_WORKFLOW_WRITABLE_FIELDS).toContain('tags');
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('does NOT contain read-only fields', () => {
|
|
462
|
+
const readOnlyFields = ['id', 'active', 'createdAt', 'updatedAt', 'versionId'];
|
|
463
|
+
for (const field of readOnlyFields) {
|
|
464
|
+
expect(N8N_WORKFLOW_WRITABLE_FIELDS).not.toContain(field);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
});
|
package/src/n8n-client.ts
CHANGED
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
* Clean, minimal implementation with built-in safety checks
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
import {
|
|
7
|
+
N8N_WORKFLOW_WRITABLE_FIELDS,
|
|
8
|
+
pickFields,
|
|
9
|
+
type N8nWorkflow,
|
|
10
|
+
type N8nWorkflowListItem,
|
|
11
|
+
type N8nExecution,
|
|
12
|
+
type N8nExecutionListItem,
|
|
13
|
+
type N8nListResponse,
|
|
14
|
+
type N8nNode,
|
|
15
|
+
type N8nNodeType,
|
|
16
|
+
type PatchOperation,
|
|
15
17
|
} from './types.js';
|
|
16
18
|
|
|
17
19
|
export interface N8nClientConfig {
|
|
@@ -98,8 +100,8 @@ export class N8nClient {
|
|
|
98
100
|
id: string,
|
|
99
101
|
workflow: Partial<N8nWorkflow>
|
|
100
102
|
): Promise<N8nWorkflow> {
|
|
101
|
-
//
|
|
102
|
-
const
|
|
103
|
+
// Schema-driven: only send fields n8n accepts (defined in types.ts)
|
|
104
|
+
const allowed = pickFields(workflow, N8N_WORKFLOW_WRITABLE_FIELDS);
|
|
103
105
|
return this.request('PUT', `/api/v1/workflows/${id}`, allowed);
|
|
104
106
|
}
|
|
105
107
|
|
package/src/types.ts
CHANGED
|
@@ -106,6 +106,49 @@ export interface N8nListResponse<T> {
|
|
|
106
106
|
nextCursor?: string;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
// ─────────────────────────────────────────────────────────────
|
|
110
|
+
// Schema-driven field definitions
|
|
111
|
+
// Source of truth: n8n OpenAPI spec at /api/v1/openapi.yml
|
|
112
|
+
// ─────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Fields that n8n API accepts on PUT /workflows/:id
|
|
116
|
+
* All other fields (id, createdAt, homeProject, etc.) are read-only
|
|
117
|
+
*
|
|
118
|
+
* Derived from: n8n OpenAPI spec (GET {n8n-url}/api/v1/openapi.yml)
|
|
119
|
+
* Path: /workflows/{id} → put → requestBody → content → application/json → schema
|
|
120
|
+
*
|
|
121
|
+
* If n8n adds new writable fields, check the OpenAPI spec and update this array.
|
|
122
|
+
*/
|
|
123
|
+
export const N8N_WORKFLOW_WRITABLE_FIELDS = [
|
|
124
|
+
'name',
|
|
125
|
+
'nodes',
|
|
126
|
+
'connections',
|
|
127
|
+
'settings',
|
|
128
|
+
'staticData',
|
|
129
|
+
'tags',
|
|
130
|
+
] as const;
|
|
131
|
+
|
|
132
|
+
export type N8nWorkflowWritableField = (typeof N8N_WORKFLOW_WRITABLE_FIELDS)[number];
|
|
133
|
+
export type N8nWorkflowUpdate = Pick<N8nWorkflow, N8nWorkflowWritableField>;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Pick only specified fields from an object (strips everything else)
|
|
137
|
+
* Generic utility for schema-driven field filtering
|
|
138
|
+
*/
|
|
139
|
+
export function pickFields<T, K extends keyof T>(
|
|
140
|
+
obj: T,
|
|
141
|
+
fields: readonly K[]
|
|
142
|
+
): Pick<T, K> {
|
|
143
|
+
const result = {} as Pick<T, K>;
|
|
144
|
+
for (const field of fields) {
|
|
145
|
+
if (field in (obj as object) && obj[field] !== undefined) {
|
|
146
|
+
result[field] = obj[field];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
|
|
109
152
|
// Node type information from n8n API (GET /api/v1/nodes)
|
|
110
153
|
export interface N8nNodeType {
|
|
111
154
|
name: string; // e.g., "n8n-nodes-base.webhook"
|