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
@@ -14,6 +14,7 @@ import {
|
|||||||
TagListParams,
|
TagListParams,
|
||||||
TagListResponse,
|
TagListResponse,
|
||||||
HealthCheckResponse,
|
HealthCheckResponse,
|
||||||
|
N8nVersionInfo,
|
||||||
Variable,
|
Variable,
|
||||||
WebhookRequest,
|
WebhookRequest,
|
||||||
WorkflowExport,
|
WorkflowExport,
|
||||||
@@ -24,6 +25,11 @@ import {
|
|||||||
} from '../types/n8n-api';
|
} from '../types/n8n-api';
|
||||||
import { handleN8nApiError, logN8nError } from '../utils/n8n-errors';
|
import { handleN8nApiError, logN8nError } from '../utils/n8n-errors';
|
||||||
import { cleanWorkflowForCreate, cleanWorkflowForUpdate } from './n8n-validation';
|
import { cleanWorkflowForCreate, cleanWorkflowForUpdate } from './n8n-validation';
|
||||||
|
import {
|
||||||
|
fetchN8nVersion,
|
||||||
|
cleanSettingsForVersion,
|
||||||
|
getCachedVersion,
|
||||||
|
} from './n8n-version';
|
||||||
|
|
||||||
export interface N8nApiClientConfig {
|
export interface N8nApiClientConfig {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
@@ -35,11 +41,15 @@ export interface N8nApiClientConfig {
|
|||||||
export class N8nApiClient {
|
export class N8nApiClient {
|
||||||
private client: AxiosInstance;
|
private client: AxiosInstance;
|
||||||
private maxRetries: number;
|
private maxRetries: number;
|
||||||
|
private baseUrl: string;
|
||||||
|
private versionInfo: N8nVersionInfo | null = null;
|
||||||
|
private versionFetched = false;
|
||||||
|
|
||||||
constructor(config: N8nApiClientConfig) {
|
constructor(config: N8nApiClientConfig) {
|
||||||
const { baseUrl, apiKey, timeout = 30000, maxRetries = 3 } = config;
|
const { baseUrl, apiKey, timeout = 30000, maxRetries = 3 } = config;
|
||||||
|
|
||||||
this.maxRetries = maxRetries;
|
this.maxRetries = maxRetries;
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
|
||||||
// Ensure baseUrl ends with /api/v1
|
// Ensure baseUrl ends with /api/v1
|
||||||
const apiUrl = baseUrl.endsWith('/api/v1')
|
const apiUrl = baseUrl.endsWith('/api/v1')
|
||||||
@@ -84,6 +94,29 @@ export class N8nApiClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the n8n version, fetching it if not already cached
|
||||||
|
*/
|
||||||
|
async getVersion(): Promise<N8nVersionInfo | null> {
|
||||||
|
if (!this.versionFetched) {
|
||||||
|
// Check if already cached globally
|
||||||
|
this.versionInfo = getCachedVersion(this.baseUrl);
|
||||||
|
if (!this.versionInfo) {
|
||||||
|
// Fetch from server
|
||||||
|
this.versionInfo = await fetchN8nVersion(this.baseUrl);
|
||||||
|
}
|
||||||
|
this.versionFetched = true;
|
||||||
|
}
|
||||||
|
return this.versionInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cached version info without fetching
|
||||||
|
*/
|
||||||
|
getCachedVersionInfo(): N8nVersionInfo | null {
|
||||||
|
return this.versionInfo;
|
||||||
|
}
|
||||||
|
|
||||||
// Health check to verify API connectivity
|
// Health check to verify API connectivity
|
||||||
async healthCheck(): Promise<HealthCheckResponse> {
|
async healthCheck(): Promise<HealthCheckResponse> {
|
||||||
try {
|
try {
|
||||||
@@ -96,10 +129,14 @@ export class N8nApiClient {
|
|||||||
validateStatus: (status) => status < 500
|
validateStatus: (status) => status < 500
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Also fetch version info (will be cached)
|
||||||
|
const versionInfo = await this.getVersion();
|
||||||
|
|
||||||
if (response.status === 200 && response.data?.status === 'ok') {
|
if (response.status === 200 && response.data?.status === 'ok') {
|
||||||
return {
|
return {
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
features: {} // Features detection would require additional endpoints
|
n8nVersion: versionInfo?.version,
|
||||||
|
features: {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,8 +147,13 @@ export class N8nApiClient {
|
|||||||
// This is a fallback for older n8n versions
|
// This is a fallback for older n8n versions
|
||||||
try {
|
try {
|
||||||
await this.client.get('/workflows', { params: { limit: 1 } });
|
await this.client.get('/workflows', { params: { limit: 1 } });
|
||||||
|
|
||||||
|
// Still try to get version
|
||||||
|
const versionInfo = await this.getVersion();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
|
n8nVersion: versionInfo?.version,
|
||||||
features: {}
|
features: {}
|
||||||
};
|
};
|
||||||
} catch (fallbackError) {
|
} catch (fallbackError) {
|
||||||
@@ -142,8 +184,25 @@ export class N8nApiClient {
|
|||||||
|
|
||||||
async updateWorkflow(id: string, workflow: Partial<Workflow>): Promise<Workflow> {
|
async updateWorkflow(id: string, workflow: Partial<Workflow>): Promise<Workflow> {
|
||||||
try {
|
try {
|
||||||
// First, try PUT method (newer n8n versions)
|
// Step 1: Basic cleaning (remove read-only fields, filter to known settings)
|
||||||
const cleanedWorkflow = cleanWorkflowForUpdate(workflow as Workflow);
|
const cleanedWorkflow = cleanWorkflowForUpdate(workflow as Workflow);
|
||||||
|
|
||||||
|
// Step 2: Version-aware settings filtering for older n8n compatibility
|
||||||
|
// This prevents "additional properties" errors on n8n < 1.119.0
|
||||||
|
const versionInfo = await this.getVersion();
|
||||||
|
if (versionInfo) {
|
||||||
|
logger.debug(`Updating workflow with n8n version ${versionInfo.version}`);
|
||||||
|
// Apply version-specific filtering to settings
|
||||||
|
cleanedWorkflow.settings = cleanSettingsForVersion(
|
||||||
|
cleanedWorkflow.settings as Record<string, unknown>,
|
||||||
|
versionInfo
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.warn('Could not determine n8n version, sending all known settings properties');
|
||||||
|
// Without version info, we send all known properties (might fail on old n8n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, try PUT method (newer n8n versions)
|
||||||
try {
|
try {
|
||||||
const response = await this.client.put(`/workflows/${id}`, cleanedWorkflow);
|
const response = await this.client.put(`/workflows/${id}`, cleanedWorkflow);
|
||||||
return response.data;
|
return response.data;
|
||||||
@@ -288,7 +347,7 @@ export class N8nApiClient {
|
|||||||
// Create a new axios instance for webhook requests to avoid API interceptors
|
// Create a new axios instance for webhook requests to avoid API interceptors
|
||||||
const webhookClient = axios.create({
|
const webhookClient = axios.create({
|
||||||
baseURL: new URL('/', webhookUrl).toString(),
|
baseURL: new URL('/', webhookUrl).toString(),
|
||||||
validateStatus: (status) => status < 500, // Don't throw on 4xx
|
validateStatus: (status: number) => status < 500, // Don't throw on 4xx
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await webhookClient.request(config);
|
const response = await webhookClient.request(config);
|
||||||
|
|||||||
@@ -116,102 +116,73 @@ export function cleanWorkflowForCreate(workflow: Partial<Workflow>): Partial<Wor
|
|||||||
* Clean workflow data for update operations.
|
* Clean workflow data for update operations.
|
||||||
*
|
*
|
||||||
* This function removes read-only and computed fields that should not be sent
|
* This function removes read-only and computed fields that should not be sent
|
||||||
* in API update requests. It does NOT add any default values or new fields.
|
* in API update requests. It filters settings to known API-accepted properties
|
||||||
|
* to prevent "additional properties" errors.
|
||||||
*
|
*
|
||||||
* Note: Unlike cleanWorkflowForCreate, this function does not add default settings.
|
* NOTE: This function filters settings to ALL known properties (12 total).
|
||||||
* The n8n API will reject update requests that include properties not present in
|
* For version-specific filtering (compatibility with older n8n versions),
|
||||||
* the original workflow ("settings must NOT have additional properties" error).
|
* use N8nApiClient.updateWorkflow() which automatically detects the n8n version
|
||||||
*
|
* and filters settings accordingly.
|
||||||
* Settings are filtered to only include whitelisted properties to prevent API
|
|
||||||
* errors when workflows from n8n contain UI-only or deprecated properties.
|
|
||||||
*
|
*
|
||||||
* @param workflow - The workflow object to clean
|
* @param workflow - The workflow object to clean
|
||||||
* @returns A cleaned partial workflow suitable for API updates
|
* @returns A cleaned partial workflow suitable for API updates
|
||||||
*/
|
*/
|
||||||
export function cleanWorkflowForUpdate(workflow: Workflow): Partial<Workflow> {
|
export function cleanWorkflowForUpdate(workflow: Workflow): Partial<Workflow> {
|
||||||
const {
|
const {
|
||||||
// Remove read-only/computed fields
|
// Remove ALL read-only/computed fields (comprehensive list)
|
||||||
id,
|
id,
|
||||||
createdAt,
|
createdAt,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
versionId,
|
versionId,
|
||||||
versionCounter, // Added: n8n 1.118.1+ returns this but rejects it in updates
|
versionCounter,
|
||||||
meta,
|
meta,
|
||||||
staticData,
|
staticData,
|
||||||
// Remove fields that cause API errors
|
|
||||||
pinData,
|
pinData,
|
||||||
tags,
|
tags,
|
||||||
description, // Issue #431: n8n returns this field but rejects it in updates
|
description,
|
||||||
// Remove additional fields that n8n API doesn't accept
|
|
||||||
isArchived,
|
isArchived,
|
||||||
usedCredentials,
|
usedCredentials,
|
||||||
sharedWithProjects,
|
sharedWithProjects,
|
||||||
triggerCount,
|
triggerCount,
|
||||||
shared,
|
shared,
|
||||||
active,
|
active,
|
||||||
// Remove version-related read-only fields (Issue #466)
|
|
||||||
activeVersionId,
|
activeVersionId,
|
||||||
activeVersion,
|
activeVersion,
|
||||||
// Keep everything else
|
// Keep everything else
|
||||||
...cleanedWorkflow
|
...cleanedWorkflow
|
||||||
} = workflow as any;
|
} = workflow as any;
|
||||||
|
|
||||||
// CRITICAL FIX for Issue #248:
|
// ALL known settings properties accepted by n8n Public API (as of n8n 1.119.0+)
|
||||||
// The n8n API has version-specific behavior for settings in workflow updates:
|
// This list is the UNION of all properties ever accepted by any n8n version
|
||||||
//
|
// Version-specific filtering is handled by N8nApiClient.updateWorkflow()
|
||||||
// PROBLEM:
|
const ALL_KNOWN_SETTINGS_PROPERTIES = new Set([
|
||||||
// - Some versions reject updates with settings properties (community forum reports)
|
// Core properties (all versions)
|
||||||
// - Properties like callerPolicy cause "additional properties" errors
|
'saveExecutionProgress',
|
||||||
// - Empty settings objects {} cause "additional properties" validation errors (Issue #431)
|
'saveManualExecutions',
|
||||||
//
|
'saveDataErrorExecution',
|
||||||
// SOLUTION:
|
'saveDataSuccessExecution',
|
||||||
// - Filter settings to only include whitelisted properties (OpenAPI spec)
|
'executionTimeout',
|
||||||
// - If no settings after filtering, omit the property entirely (n8n API rejects empty objects)
|
'errorWorkflow',
|
||||||
// - Omitting the property prevents "additional properties" validation errors
|
'timezone',
|
||||||
// - Whitelisted properties prevent "additional properties" errors
|
// Added in n8n 1.37.0
|
||||||
//
|
'executionOrder',
|
||||||
// References:
|
// Added in n8n 1.119.0
|
||||||
// - Issue #431: Empty settings validation error
|
'callerPolicy',
|
||||||
// - https://community.n8n.io/t/api-workflow-update-endpoint-doesnt-support-setting-callerpolicy/161916
|
'callerIds',
|
||||||
// - OpenAPI spec: workflowSettings schema
|
'timeSavedPerExecution',
|
||||||
// - Tested on n8n.estyl.team (cloud) and localhost (self-hosted)
|
'availableInMCP',
|
||||||
|
]);
|
||||||
// Whitelisted settings properties from n8n Public API OpenAPI spec
|
|
||||||
// Source: https://github.com/n8n-io/n8n/blob/master/packages/cli/src/public-api/v1/handlers/workflows/spec/schemas/workflowSettings.yml
|
|
||||||
//
|
|
||||||
// CRITICAL: The n8n Public API uses strict JSON schema validation with
|
|
||||||
// additionalProperties: false. These 12 properties are accepted:
|
|
||||||
const safeSettingsProperties = [
|
|
||||||
'saveExecutionProgress', // boolean
|
|
||||||
'saveManualExecutions', // boolean
|
|
||||||
'saveDataErrorExecution', // string enum: 'all' | 'none'
|
|
||||||
'saveDataSuccessExecution', // string enum: 'all' | 'none'
|
|
||||||
'executionTimeout', // number (max: 3600)
|
|
||||||
'errorWorkflow', // string (workflow ID)
|
|
||||||
'timezone', // string (e.g., 'America/New_York')
|
|
||||||
'executionOrder', // string (e.g., 'v1')
|
|
||||||
'callerPolicy', // string enum: 'any' | 'none' | 'workflowsFromAList' | 'workflowsFromSameOwner'
|
|
||||||
'callerIds', // string (comma-separated workflow IDs)
|
|
||||||
'timeSavedPerExecution', // number (in minutes)
|
|
||||||
'availableInMCP', // boolean (default: false)
|
|
||||||
];
|
|
||||||
|
|
||||||
if (cleanedWorkflow.settings && typeof cleanedWorkflow.settings === 'object') {
|
if (cleanedWorkflow.settings && typeof cleanedWorkflow.settings === 'object') {
|
||||||
// Filter to only safe properties
|
// Filter to only known properties (security + prevent garbage)
|
||||||
const filteredSettings: any = {};
|
const filteredSettings: Record<string, unknown> = {};
|
||||||
for (const key of safeSettingsProperties) {
|
for (const [key, value] of Object.entries(cleanedWorkflow.settings)) {
|
||||||
if (key in cleanedWorkflow.settings) {
|
if (ALL_KNOWN_SETTINGS_PROPERTIES.has(key)) {
|
||||||
filteredSettings[key] = (cleanedWorkflow.settings as any)[key];
|
filteredSettings[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// n8n API behavior with settings:
|
|
||||||
// - The settings property is REQUIRED by the API
|
|
||||||
// - Empty settings objects {} are now accepted (n8n preserves existing settings)
|
|
||||||
// - Only whitelisted properties are accepted; others cause "additional properties" error
|
|
||||||
cleanedWorkflow.settings = filteredSettings;
|
cleanedWorkflow.settings = filteredSettings;
|
||||||
} else {
|
} else {
|
||||||
// No settings provided - use empty object (required by API)
|
|
||||||
cleanedWorkflow.settings = {};
|
cleanedWorkflow.settings = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
225
src/services/n8n-version.ts
Normal file
225
src/services/n8n-version.ts
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
/**
|
||||||
|
* n8n Version Detection and Version-Aware Settings Filtering
|
||||||
|
*
|
||||||
|
* This module provides version detection for n8n instances and filters
|
||||||
|
* workflow settings based on what the target n8n version supports.
|
||||||
|
*
|
||||||
|
* VERSION HISTORY for workflowSettings in n8n Public API:
|
||||||
|
* - All versions: 7 core properties (saveExecutionProgress, saveManualExecutions,
|
||||||
|
* saveDataErrorExecution, saveDataSuccessExecution, executionTimeout,
|
||||||
|
* errorWorkflow, timezone)
|
||||||
|
* - 1.37.0+: Added executionOrder
|
||||||
|
* - 1.119.0+: Added callerPolicy, callerIds, timeSavedPerExecution, availableInMCP
|
||||||
|
*
|
||||||
|
* References:
|
||||||
|
* - https://github.com/n8n-io/n8n/pull/21297 (PR adding 4 new properties in 1.119.0)
|
||||||
|
* - https://community.n8n.io/t/n8n-api-update-workflow-does-not-accept-executionorder-setting/44512
|
||||||
|
*/
|
||||||
|
|
||||||
|
import axios from 'axios';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
import { N8nVersionInfo, N8nSettingsResponse } from '../types/n8n-api';
|
||||||
|
|
||||||
|
// Cache version info per base URL to avoid repeated API calls
|
||||||
|
const versionCache = new Map<string, N8nVersionInfo>();
|
||||||
|
|
||||||
|
// Settings properties supported by each n8n version range
|
||||||
|
// These are CUMULATIVE - each version adds to the previous
|
||||||
|
const SETTINGS_BY_VERSION = {
|
||||||
|
// Core properties supported by all versions
|
||||||
|
core: [
|
||||||
|
'saveExecutionProgress',
|
||||||
|
'saveManualExecutions',
|
||||||
|
'saveDataErrorExecution',
|
||||||
|
'saveDataSuccessExecution',
|
||||||
|
'executionTimeout',
|
||||||
|
'errorWorkflow',
|
||||||
|
'timezone',
|
||||||
|
],
|
||||||
|
// Added in n8n 1.37.0
|
||||||
|
v1_37_0: [
|
||||||
|
'executionOrder',
|
||||||
|
],
|
||||||
|
// Added in n8n 1.119.0 (PR #21297)
|
||||||
|
v1_119_0: [
|
||||||
|
'callerPolicy',
|
||||||
|
'callerIds',
|
||||||
|
'timeSavedPerExecution',
|
||||||
|
'availableInMCP',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse version string into structured version info
|
||||||
|
*/
|
||||||
|
export function parseVersion(versionString: string): N8nVersionInfo | null {
|
||||||
|
// Handle formats like "1.119.0", "1.37.0-beta.1", "0.200.0"
|
||||||
|
const match = versionString.match(/^(\d+)\.(\d+)\.(\d+)/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: versionString,
|
||||||
|
major: parseInt(match[1], 10),
|
||||||
|
minor: parseInt(match[2], 10),
|
||||||
|
patch: parseInt(match[3], 10),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare two versions: returns -1 if a < b, 0 if equal, 1 if a > b
|
||||||
|
*/
|
||||||
|
export function compareVersions(a: N8nVersionInfo, b: N8nVersionInfo): number {
|
||||||
|
if (a.major !== b.major) return a.major - b.major;
|
||||||
|
if (a.minor !== b.minor) return a.minor - b.minor;
|
||||||
|
return a.patch - b.patch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if version meets minimum requirement
|
||||||
|
*/
|
||||||
|
export function versionAtLeast(version: N8nVersionInfo, major: number, minor: number, patch = 0): boolean {
|
||||||
|
const target = { version: '', major, minor, patch };
|
||||||
|
return compareVersions(version, target) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get supported settings properties for a given n8n version
|
||||||
|
*/
|
||||||
|
export function getSupportedSettingsProperties(version: N8nVersionInfo): Set<string> {
|
||||||
|
const supported = new Set<string>(SETTINGS_BY_VERSION.core);
|
||||||
|
|
||||||
|
// Add executionOrder if >= 1.37.0
|
||||||
|
if (versionAtLeast(version, 1, 37, 0)) {
|
||||||
|
SETTINGS_BY_VERSION.v1_37_0.forEach(prop => supported.add(prop));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new properties if >= 1.119.0
|
||||||
|
if (versionAtLeast(version, 1, 119, 0)) {
|
||||||
|
SETTINGS_BY_VERSION.v1_119_0.forEach(prop => supported.add(prop));
|
||||||
|
}
|
||||||
|
|
||||||
|
return supported;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch n8n version from /rest/settings endpoint
|
||||||
|
*
|
||||||
|
* This endpoint is available on all n8n instances and doesn't require authentication.
|
||||||
|
* Note: There's a security concern about this being unauthenticated (see n8n community),
|
||||||
|
* but it's the only reliable way to get version info.
|
||||||
|
*/
|
||||||
|
export async function fetchN8nVersion(baseUrl: string): Promise<N8nVersionInfo | null> {
|
||||||
|
// Check cache first
|
||||||
|
const cached = versionCache.get(baseUrl);
|
||||||
|
if (cached) {
|
||||||
|
logger.debug(`Using cached n8n version for ${baseUrl}: ${cached.version}`);
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Remove /api/v1 suffix if present to get base URL
|
||||||
|
const cleanBaseUrl = baseUrl.replace(/\/api\/v\d+\/?$/, '').replace(/\/$/, '');
|
||||||
|
const settingsUrl = `${cleanBaseUrl}/rest/settings`;
|
||||||
|
|
||||||
|
logger.debug(`Fetching n8n version from ${settingsUrl}`);
|
||||||
|
|
||||||
|
const response = await axios.get<N8nSettingsResponse>(settingsUrl, {
|
||||||
|
timeout: 5000,
|
||||||
|
validateStatus: (status: number) => status < 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 200 && response.data) {
|
||||||
|
// n8n wraps the settings in a "data" property
|
||||||
|
const settings = response.data.data;
|
||||||
|
if (!settings) {
|
||||||
|
logger.warn('No data in settings response');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// n8n can return version in different fields
|
||||||
|
const versionString = settings.n8nVersion || settings.versionCli;
|
||||||
|
|
||||||
|
if (versionString) {
|
||||||
|
const versionInfo = parseVersion(versionString);
|
||||||
|
if (versionInfo) {
|
||||||
|
// Cache the result
|
||||||
|
versionCache.set(baseUrl, versionInfo);
|
||||||
|
logger.debug(`Detected n8n version: ${versionInfo.version}`);
|
||||||
|
return versionInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn(`Could not determine n8n version from ${settingsUrl}`);
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`Failed to fetch n8n version: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear version cache (useful for testing or when server changes)
|
||||||
|
*/
|
||||||
|
export function clearVersionCache(): void {
|
||||||
|
versionCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cached version for a base URL (or null if not cached)
|
||||||
|
*/
|
||||||
|
export function getCachedVersion(baseUrl: string): N8nVersionInfo | null {
|
||||||
|
return versionCache.get(baseUrl) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set cached version (useful for testing or when version is known)
|
||||||
|
*/
|
||||||
|
export function setCachedVersion(baseUrl: string, version: N8nVersionInfo): void {
|
||||||
|
versionCache.set(baseUrl, version);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean workflow settings for API update based on n8n version
|
||||||
|
*
|
||||||
|
* This function filters workflow settings to only include properties
|
||||||
|
* that the target n8n version supports, preventing "additional properties" errors.
|
||||||
|
*
|
||||||
|
* @param settings - The workflow settings to clean
|
||||||
|
* @param version - The target n8n version (if null, returns settings unchanged)
|
||||||
|
* @returns Cleaned settings object
|
||||||
|
*/
|
||||||
|
export function cleanSettingsForVersion(
|
||||||
|
settings: Record<string, unknown> | undefined,
|
||||||
|
version: N8nVersionInfo | null
|
||||||
|
): Record<string, unknown> {
|
||||||
|
if (!settings || typeof settings !== 'object') {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If version unknown, return settings unchanged (let the API decide)
|
||||||
|
if (!version) {
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
const supportedProperties = getSupportedSettingsProperties(version);
|
||||||
|
|
||||||
|
const cleaned: Record<string, unknown> = {};
|
||||||
|
for (const [key, value] of Object.entries(settings)) {
|
||||||
|
if (supportedProperties.has(key)) {
|
||||||
|
cleaned[key] = value;
|
||||||
|
} else {
|
||||||
|
logger.debug(`Filtered out unsupported settings property: ${key} (n8n ${version.version})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export version thresholds for testing
|
||||||
|
export const VERSION_THRESHOLDS = {
|
||||||
|
EXECUTION_ORDER: { major: 1, minor: 37, patch: 0 },
|
||||||
|
CALLER_POLICY: { major: 1, minor: 119, patch: 0 },
|
||||||
|
};
|
||||||
@@ -225,6 +225,28 @@ export interface HealthCheckResponse {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// n8n Version Information
|
||||||
|
export interface N8nVersionInfo {
|
||||||
|
version: string; // Full version string, e.g., "1.119.0"
|
||||||
|
major: number; // Major version number
|
||||||
|
minor: number; // Minor version number
|
||||||
|
patch: number; // Patch version number
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings data within the response
|
||||||
|
export interface N8nSettingsData {
|
||||||
|
n8nVersion?: string;
|
||||||
|
versionCli?: string;
|
||||||
|
instanceId?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response from /rest/settings endpoint (unauthenticated)
|
||||||
|
// The actual response wraps settings in a "data" property
|
||||||
|
export interface N8nSettingsResponse {
|
||||||
|
data?: N8nSettingsData;
|
||||||
|
}
|
||||||
|
|
||||||
// Request Parameter Types
|
// Request Parameter Types
|
||||||
export interface WorkflowListParams {
|
export interface WorkflowListParams {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
|||||||
@@ -383,7 +383,7 @@ describe('n8n-validation', () => {
|
|||||||
expect(cleaned.name).toBe('Test Workflow');
|
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 = {
|
const workflow = {
|
||||||
name: 'Test Workflow',
|
name: 'Test Workflow',
|
||||||
nodes: [],
|
nodes: [],
|
||||||
@@ -391,8 +391,8 @@ describe('n8n-validation', () => {
|
|||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
const cleaned = cleanWorkflowForUpdate(workflow);
|
const cleaned = cleanWorkflowForUpdate(workflow);
|
||||||
// n8n API requires settings to be present, so we provide minimal defaults (v1 is modern default)
|
// n8n API now accepts empty settings {} - server preserves existing values
|
||||||
expect(cleaned.settings).toEqual({ executionOrder: 'v1' });
|
expect(cleaned.settings).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should filter settings to safe properties to prevent API errors (Issue #248 - final fix)', () => {
|
it('should filter settings to safe properties to prevent API errors (Issue #248 - final fix)', () => {
|
||||||
@@ -403,20 +403,22 @@ describe('n8n-validation', () => {
|
|||||||
settings: {
|
settings: {
|
||||||
executionOrder: 'v1' as const,
|
executionOrder: 'v1' as const,
|
||||||
saveDataSuccessExecution: 'none' as const,
|
saveDataSuccessExecution: 'none' as const,
|
||||||
callerPolicy: 'workflowsFromSameOwner' as const, // Now whitelisted (n8n 1.121+)
|
callerPolicy: 'workflowsFromSameOwner' as const, // Whitelisted (n8n 1.119+)
|
||||||
timeSavedPerExecution: 5, // Filtered out (UI-only property)
|
timeSavedPerExecution: 5, // Whitelisted (n8n 1.119+, PR #21297)
|
||||||
|
unknownProperty: 'should be filtered', // Unknown properties ARE filtered
|
||||||
},
|
},
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
const cleaned = cleanWorkflowForUpdate(workflow);
|
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({
|
expect(cleaned.settings).toEqual({
|
||||||
executionOrder: 'v1',
|
executionOrder: 'v1',
|
||||||
saveDataSuccessExecution: 'none',
|
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)', () => {
|
it('should preserve callerPolicy and availableInMCP (n8n 1.121+ settings)', () => {
|
||||||
@@ -487,26 +489,26 @@ describe('n8n-validation', () => {
|
|||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
const cleaned = cleanWorkflowForUpdate(workflow);
|
const cleaned = cleanWorkflowForUpdate(workflow);
|
||||||
// n8n API requires settings, so we provide minimal defaults (v1 is modern default)
|
// n8n API now accepts empty settings {} - server preserves existing values
|
||||||
expect(cleaned.settings).toEqual({ executionOrder: 'v1' });
|
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 = {
|
const workflow = {
|
||||||
name: 'Test Workflow',
|
name: 'Test Workflow',
|
||||||
nodes: [],
|
nodes: [],
|
||||||
connections: {},
|
connections: {},
|
||||||
settings: {
|
settings: {
|
||||||
timeSavedPerExecution: 5, // Filtered out (UI-only)
|
timeSavedPerExecution: 5, // Whitelisted (n8n 1.119+)
|
||||||
someOtherProperty: 'value', // Filtered out
|
someOtherProperty: 'value', // Filtered out (unknown)
|
||||||
},
|
},
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
const cleaned = cleanWorkflowForUpdate(workflow);
|
const cleaned = cleanWorkflowForUpdate(workflow);
|
||||||
// All properties were filtered out, but n8n API requires settings
|
// timeSavedPerExecution is now whitelisted, someOtherProperty is filtered out
|
||||||
// so we provide minimal defaults (v1 is modern default) to avoid both
|
// n8n API now accepts empty or partial settings {} - server preserves existing values
|
||||||
// "additional properties" and "required property" API errors
|
expect(cleaned.settings).toEqual({ timeSavedPerExecution: 5 });
|
||||||
expect(cleaned.settings).toEqual({ executionOrder: 'v1' });
|
expect(cleaned.settings).not.toHaveProperty('someOtherProperty');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should preserve whitelisted settings when mixed with non-whitelisted (Issue #431)', () => {
|
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('active');
|
||||||
expect(forUpdate).not.toHaveProperty('tags');
|
expect(forUpdate).not.toHaveProperty('tags');
|
||||||
expect(forUpdate).not.toHaveProperty('meta');
|
expect(forUpdate).not.toHaveProperty('meta');
|
||||||
// n8n API requires settings in updates, so minimal defaults (v1) are provided (Issue #431)
|
// n8n API now accepts empty settings {} - server preserves existing values
|
||||||
expect(forUpdate.settings).toEqual({ executionOrder: 'v1' });
|
expect(forUpdate.settings).toEqual({});
|
||||||
expect(validateWorkflowStructure(forUpdate)).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