mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 06:22:04 +00:00
Add version-aware settings filtering for n8n API compatibility
This commit adds automatic detection of n8n server version and filters workflow settings properties based on what the target version supports. Changes: - Add n8n-version.ts service for version detection via /rest/settings - Fix nested response structure handling (data.data.versionCli) - Add N8nVersionInfo and N8nSettingsData types - Update N8nApiClient with getVersion() and version-aware filtering - Clean workflow settings before API update based on detected version Version compatibility: - n8n < 1.37.0: 7 core properties only - n8n 1.37.0+: adds executionOrder - n8n 1.119.0+: adds callerPolicy, callerIds, timeSavedPerExecution, availableInMCP Fixes #466
This commit is contained in:
committed by
Romuald Członkowski
parent
934124fa7b
commit
a70d96a373
@@ -383,7 +383,7 @@ describe('n8n-validation', () => {
|
||||
expect(cleaned.name).toBe('Test Workflow');
|
||||
});
|
||||
|
||||
it('should provide minimal default settings when no settings provided (Issue #431)', () => {
|
||||
it('should provide empty settings when no settings provided (Issue #431)', () => {
|
||||
const workflow = {
|
||||
name: 'Test Workflow',
|
||||
nodes: [],
|
||||
@@ -391,8 +391,8 @@ describe('n8n-validation', () => {
|
||||
} as any;
|
||||
|
||||
const cleaned = cleanWorkflowForUpdate(workflow);
|
||||
// n8n API requires settings to be present, so we provide minimal defaults (v1 is modern default)
|
||||
expect(cleaned.settings).toEqual({ executionOrder: 'v1' });
|
||||
// n8n API now accepts empty settings {} - server preserves existing values
|
||||
expect(cleaned.settings).toEqual({});
|
||||
});
|
||||
|
||||
it('should filter settings to safe properties to prevent API errors (Issue #248 - final fix)', () => {
|
||||
@@ -403,20 +403,22 @@ describe('n8n-validation', () => {
|
||||
settings: {
|
||||
executionOrder: 'v1' as const,
|
||||
saveDataSuccessExecution: 'none' as const,
|
||||
callerPolicy: 'workflowsFromSameOwner' as const, // Now whitelisted (n8n 1.121+)
|
||||
timeSavedPerExecution: 5, // Filtered out (UI-only property)
|
||||
callerPolicy: 'workflowsFromSameOwner' as const, // Whitelisted (n8n 1.119+)
|
||||
timeSavedPerExecution: 5, // Whitelisted (n8n 1.119+, PR #21297)
|
||||
unknownProperty: 'should be filtered', // Unknown properties ARE filtered
|
||||
},
|
||||
} as any;
|
||||
|
||||
const cleaned = cleanWorkflowForUpdate(workflow);
|
||||
|
||||
// Unsafe properties filtered out, safe properties kept (callerPolicy now whitelisted)
|
||||
// All 4 properties from n8n 1.119+ are whitelisted, unknown properties filtered
|
||||
expect(cleaned.settings).toEqual({
|
||||
executionOrder: 'v1',
|
||||
saveDataSuccessExecution: 'none',
|
||||
callerPolicy: 'workflowsFromSameOwner'
|
||||
callerPolicy: 'workflowsFromSameOwner',
|
||||
timeSavedPerExecution: 5,
|
||||
});
|
||||
expect(cleaned.settings).not.toHaveProperty('timeSavedPerExecution');
|
||||
expect(cleaned.settings).not.toHaveProperty('unknownProperty');
|
||||
});
|
||||
|
||||
it('should preserve callerPolicy and availableInMCP (n8n 1.121+ settings)', () => {
|
||||
@@ -487,26 +489,26 @@ describe('n8n-validation', () => {
|
||||
} as any;
|
||||
|
||||
const cleaned = cleanWorkflowForUpdate(workflow);
|
||||
// n8n API requires settings, so we provide minimal defaults (v1 is modern default)
|
||||
expect(cleaned.settings).toEqual({ executionOrder: 'v1' });
|
||||
// n8n API now accepts empty settings {} - server preserves existing values
|
||||
expect(cleaned.settings).toEqual({});
|
||||
});
|
||||
|
||||
it('should provide minimal settings when only non-whitelisted properties exist (Issue #431)', () => {
|
||||
it('should return empty settings when only non-whitelisted properties exist (Issue #431)', () => {
|
||||
const workflow = {
|
||||
name: 'Test Workflow',
|
||||
nodes: [],
|
||||
connections: {},
|
||||
settings: {
|
||||
timeSavedPerExecution: 5, // Filtered out (UI-only)
|
||||
someOtherProperty: 'value', // Filtered out
|
||||
timeSavedPerExecution: 5, // Whitelisted (n8n 1.119+)
|
||||
someOtherProperty: 'value', // Filtered out (unknown)
|
||||
},
|
||||
} as any;
|
||||
|
||||
const cleaned = cleanWorkflowForUpdate(workflow);
|
||||
// All properties were filtered out, but n8n API requires settings
|
||||
// so we provide minimal defaults (v1 is modern default) to avoid both
|
||||
// "additional properties" and "required property" API errors
|
||||
expect(cleaned.settings).toEqual({ executionOrder: 'v1' });
|
||||
// timeSavedPerExecution is now whitelisted, someOtherProperty is filtered out
|
||||
// n8n API now accepts empty or partial settings {} - server preserves existing values
|
||||
expect(cleaned.settings).toEqual({ timeSavedPerExecution: 5 });
|
||||
expect(cleaned.settings).not.toHaveProperty('someOtherProperty');
|
||||
});
|
||||
|
||||
it('should preserve whitelisted settings when mixed with non-whitelisted (Issue #431)', () => {
|
||||
@@ -1408,8 +1410,8 @@ describe('n8n-validation', () => {
|
||||
expect(forUpdate).not.toHaveProperty('active');
|
||||
expect(forUpdate).not.toHaveProperty('tags');
|
||||
expect(forUpdate).not.toHaveProperty('meta');
|
||||
// n8n API requires settings in updates, so minimal defaults (v1) are provided (Issue #431)
|
||||
expect(forUpdate.settings).toEqual({ executionOrder: 'v1' });
|
||||
// n8n API now accepts empty settings {} - server preserves existing values
|
||||
expect(forUpdate.settings).toEqual({});
|
||||
expect(validateWorkflowStructure(forUpdate)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
313
tests/unit/services/n8n-version.test.ts
Normal file
313
tests/unit/services/n8n-version.test.ts
Normal file
@@ -0,0 +1,313 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import {
|
||||
parseVersion,
|
||||
compareVersions,
|
||||
versionAtLeast,
|
||||
getSupportedSettingsProperties,
|
||||
cleanSettingsForVersion,
|
||||
clearVersionCache,
|
||||
setCachedVersion,
|
||||
getCachedVersion,
|
||||
VERSION_THRESHOLDS,
|
||||
} from '@/services/n8n-version';
|
||||
import type { N8nVersionInfo } from '@/types/n8n-api';
|
||||
|
||||
describe('n8n-version', () => {
|
||||
beforeEach(() => {
|
||||
clearVersionCache();
|
||||
});
|
||||
|
||||
describe('parseVersion', () => {
|
||||
it('should parse standard version strings', () => {
|
||||
expect(parseVersion('1.119.0')).toEqual({
|
||||
version: '1.119.0',
|
||||
major: 1,
|
||||
minor: 119,
|
||||
patch: 0,
|
||||
});
|
||||
|
||||
expect(parseVersion('1.37.0')).toEqual({
|
||||
version: '1.37.0',
|
||||
major: 1,
|
||||
minor: 37,
|
||||
patch: 0,
|
||||
});
|
||||
|
||||
expect(parseVersion('0.200.0')).toEqual({
|
||||
version: '0.200.0',
|
||||
major: 0,
|
||||
minor: 200,
|
||||
patch: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse beta/pre-release versions', () => {
|
||||
const result = parseVersion('1.119.0-beta.1');
|
||||
expect(result).toEqual({
|
||||
version: '1.119.0-beta.1',
|
||||
major: 1,
|
||||
minor: 119,
|
||||
patch: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null for invalid versions', () => {
|
||||
expect(parseVersion('invalid')).toBeNull();
|
||||
expect(parseVersion('')).toBeNull();
|
||||
expect(parseVersion('1.2')).toBeNull();
|
||||
expect(parseVersion('v1.2.3')).toBeNull(); // No 'v' prefix support
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareVersions', () => {
|
||||
it('should compare major versions correctly', () => {
|
||||
const v1 = parseVersion('1.0.0')!;
|
||||
const v2 = parseVersion('2.0.0')!;
|
||||
expect(compareVersions(v1, v2)).toBeLessThan(0);
|
||||
expect(compareVersions(v2, v1)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should compare minor versions correctly', () => {
|
||||
const v1 = parseVersion('1.37.0')!;
|
||||
const v2 = parseVersion('1.119.0')!;
|
||||
expect(compareVersions(v1, v2)).toBeLessThan(0);
|
||||
expect(compareVersions(v2, v1)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should compare patch versions correctly', () => {
|
||||
const v1 = parseVersion('1.119.0')!;
|
||||
const v2 = parseVersion('1.119.1')!;
|
||||
expect(compareVersions(v1, v2)).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('should return 0 for equal versions', () => {
|
||||
const v1 = parseVersion('1.119.0')!;
|
||||
const v2 = parseVersion('1.119.0')!;
|
||||
expect(compareVersions(v1, v2)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('versionAtLeast', () => {
|
||||
it('should return true when version meets requirement', () => {
|
||||
const v = parseVersion('1.119.0')!;
|
||||
expect(versionAtLeast(v, 1, 119, 0)).toBe(true);
|
||||
expect(versionAtLeast(v, 1, 37, 0)).toBe(true);
|
||||
expect(versionAtLeast(v, 1, 0, 0)).toBe(true);
|
||||
expect(versionAtLeast(v, 0, 200, 0)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when version is too old', () => {
|
||||
const v = parseVersion('1.36.0')!;
|
||||
expect(versionAtLeast(v, 1, 37, 0)).toBe(false);
|
||||
expect(versionAtLeast(v, 1, 119, 0)).toBe(false);
|
||||
expect(versionAtLeast(v, 2, 0, 0)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle edge cases at version boundaries', () => {
|
||||
const v37 = parseVersion('1.37.0')!;
|
||||
const v36 = parseVersion('1.36.99')!;
|
||||
|
||||
expect(versionAtLeast(v37, 1, 37, 0)).toBe(true);
|
||||
expect(versionAtLeast(v36, 1, 37, 0)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSupportedSettingsProperties', () => {
|
||||
it('should return core properties for old versions (< 1.37.0)', () => {
|
||||
const v = parseVersion('1.30.0')!;
|
||||
const supported = getSupportedSettingsProperties(v);
|
||||
|
||||
// Core properties should be supported
|
||||
expect(supported.has('saveExecutionProgress')).toBe(true);
|
||||
expect(supported.has('saveManualExecutions')).toBe(true);
|
||||
expect(supported.has('saveDataErrorExecution')).toBe(true);
|
||||
expect(supported.has('saveDataSuccessExecution')).toBe(true);
|
||||
expect(supported.has('executionTimeout')).toBe(true);
|
||||
expect(supported.has('errorWorkflow')).toBe(true);
|
||||
expect(supported.has('timezone')).toBe(true);
|
||||
|
||||
// executionOrder should NOT be supported
|
||||
expect(supported.has('executionOrder')).toBe(false);
|
||||
|
||||
// New properties should NOT be supported
|
||||
expect(supported.has('callerPolicy')).toBe(false);
|
||||
expect(supported.has('callerIds')).toBe(false);
|
||||
expect(supported.has('timeSavedPerExecution')).toBe(false);
|
||||
expect(supported.has('availableInMCP')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return core + executionOrder for v1.37.0+', () => {
|
||||
const v = parseVersion('1.37.0')!;
|
||||
const supported = getSupportedSettingsProperties(v);
|
||||
|
||||
// Core properties
|
||||
expect(supported.has('saveExecutionProgress')).toBe(true);
|
||||
expect(supported.has('timezone')).toBe(true);
|
||||
|
||||
// executionOrder should be supported
|
||||
expect(supported.has('executionOrder')).toBe(true);
|
||||
|
||||
// New properties should NOT be supported
|
||||
expect(supported.has('callerPolicy')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return all properties for v1.119.0+', () => {
|
||||
const v = parseVersion('1.119.0')!;
|
||||
const supported = getSupportedSettingsProperties(v);
|
||||
|
||||
// All 12 properties should be supported
|
||||
expect(supported.has('saveExecutionProgress')).toBe(true);
|
||||
expect(supported.has('saveManualExecutions')).toBe(true);
|
||||
expect(supported.has('saveDataErrorExecution')).toBe(true);
|
||||
expect(supported.has('saveDataSuccessExecution')).toBe(true);
|
||||
expect(supported.has('executionTimeout')).toBe(true);
|
||||
expect(supported.has('errorWorkflow')).toBe(true);
|
||||
expect(supported.has('timezone')).toBe(true);
|
||||
expect(supported.has('executionOrder')).toBe(true);
|
||||
expect(supported.has('callerPolicy')).toBe(true);
|
||||
expect(supported.has('callerIds')).toBe(true);
|
||||
expect(supported.has('timeSavedPerExecution')).toBe(true);
|
||||
expect(supported.has('availableInMCP')).toBe(true);
|
||||
|
||||
expect(supported.size).toBe(12);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanSettingsForVersion', () => {
|
||||
const fullSettings = {
|
||||
saveExecutionProgress: false,
|
||||
saveManualExecutions: true,
|
||||
saveDataErrorExecution: 'all',
|
||||
saveDataSuccessExecution: 'none',
|
||||
executionTimeout: 3600,
|
||||
errorWorkflow: '',
|
||||
timezone: 'UTC',
|
||||
executionOrder: 'v1',
|
||||
callerPolicy: 'workflowsFromSameOwner',
|
||||
callerIds: '',
|
||||
timeSavedPerExecution: 0,
|
||||
availableInMCP: false,
|
||||
};
|
||||
|
||||
it('should filter to core properties for old versions', () => {
|
||||
const v = parseVersion('1.30.0')!;
|
||||
const cleaned = cleanSettingsForVersion(fullSettings, v);
|
||||
|
||||
expect(Object.keys(cleaned)).toHaveLength(7);
|
||||
expect(cleaned).toHaveProperty('saveExecutionProgress');
|
||||
expect(cleaned).toHaveProperty('timezone');
|
||||
expect(cleaned).not.toHaveProperty('executionOrder');
|
||||
expect(cleaned).not.toHaveProperty('callerPolicy');
|
||||
});
|
||||
|
||||
it('should include executionOrder for v1.37.0+', () => {
|
||||
const v = parseVersion('1.37.0')!;
|
||||
const cleaned = cleanSettingsForVersion(fullSettings, v);
|
||||
|
||||
expect(Object.keys(cleaned)).toHaveLength(8);
|
||||
expect(cleaned).toHaveProperty('executionOrder');
|
||||
expect(cleaned).not.toHaveProperty('callerPolicy');
|
||||
});
|
||||
|
||||
it('should include all properties for v1.119.0+', () => {
|
||||
const v = parseVersion('1.119.0')!;
|
||||
const cleaned = cleanSettingsForVersion(fullSettings, v);
|
||||
|
||||
expect(Object.keys(cleaned)).toHaveLength(12);
|
||||
expect(cleaned).toHaveProperty('callerPolicy');
|
||||
expect(cleaned).toHaveProperty('availableInMCP');
|
||||
});
|
||||
|
||||
it('should return settings unchanged when version is null', () => {
|
||||
// When version unknown, return settings unchanged (let API decide)
|
||||
const cleaned = cleanSettingsForVersion(fullSettings, null);
|
||||
expect(cleaned).toEqual(fullSettings);
|
||||
});
|
||||
|
||||
it('should handle empty settings', () => {
|
||||
const v = parseVersion('1.119.0')!;
|
||||
expect(cleanSettingsForVersion({}, v)).toEqual({});
|
||||
expect(cleanSettingsForVersion(undefined, v)).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Version cache', () => {
|
||||
it('should cache and retrieve versions', () => {
|
||||
const baseUrl = 'http://localhost:5678';
|
||||
const version: N8nVersionInfo = {
|
||||
version: '1.119.0',
|
||||
major: 1,
|
||||
minor: 119,
|
||||
patch: 0,
|
||||
};
|
||||
|
||||
expect(getCachedVersion(baseUrl)).toBeNull();
|
||||
|
||||
setCachedVersion(baseUrl, version);
|
||||
expect(getCachedVersion(baseUrl)).toEqual(version);
|
||||
|
||||
clearVersionCache();
|
||||
expect(getCachedVersion(baseUrl)).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle multiple base URLs', () => {
|
||||
const url1 = 'http://localhost:5678';
|
||||
const url2 = 'http://production:5678';
|
||||
|
||||
const v1: N8nVersionInfo = { version: '1.119.0', major: 1, minor: 119, patch: 0 };
|
||||
const v2: N8nVersionInfo = { version: '1.37.0', major: 1, minor: 37, patch: 0 };
|
||||
|
||||
setCachedVersion(url1, v1);
|
||||
setCachedVersion(url2, v2);
|
||||
|
||||
expect(getCachedVersion(url1)).toEqual(v1);
|
||||
expect(getCachedVersion(url2)).toEqual(v2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('VERSION_THRESHOLDS', () => {
|
||||
it('should have correct threshold values', () => {
|
||||
expect(VERSION_THRESHOLDS.EXECUTION_ORDER).toEqual({ major: 1, minor: 37, patch: 0 });
|
||||
expect(VERSION_THRESHOLDS.CALLER_POLICY).toEqual({ major: 1, minor: 119, patch: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Real-world version scenarios', () => {
|
||||
it('should handle n8n cloud versions', () => {
|
||||
// Cloud typically runs latest
|
||||
const cloudVersion = parseVersion('1.125.0')!;
|
||||
const supported = getSupportedSettingsProperties(cloudVersion);
|
||||
expect(supported.size).toBe(12);
|
||||
});
|
||||
|
||||
it('should handle self-hosted older versions', () => {
|
||||
// Common self-hosted older version
|
||||
const selfHosted = parseVersion('1.50.0')!;
|
||||
const supported = getSupportedSettingsProperties(selfHosted);
|
||||
|
||||
expect(supported.has('executionOrder')).toBe(true);
|
||||
expect(supported.has('callerPolicy')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle workflow migration scenario', () => {
|
||||
// Workflow from n8n 1.119+ with all settings
|
||||
const fullSettings = {
|
||||
saveExecutionProgress: true,
|
||||
executionOrder: 'v1',
|
||||
callerPolicy: 'workflowsFromSameOwner',
|
||||
callerIds: '',
|
||||
timeSavedPerExecution: 5,
|
||||
availableInMCP: true,
|
||||
};
|
||||
|
||||
// Updating to n8n 1.100 (older)
|
||||
const targetVersion = parseVersion('1.100.0')!;
|
||||
const cleaned = cleanSettingsForVersion(fullSettings, targetVersion);
|
||||
|
||||
// Should filter out properties not supported in 1.100
|
||||
expect(cleaned).toHaveProperty('executionOrder');
|
||||
expect(cleaned).not.toHaveProperty('callerPolicy');
|
||||
expect(cleaned).not.toHaveProperty('availableInMCP');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user