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:
thesved
2025-12-04 19:49:18 +02:00
committed by Romuald Członkowski
parent 934124fa7b
commit a70d96a373
6 changed files with 684 additions and 92 deletions

View File

@@ -14,6 +14,7 @@ import {
TagListParams,
TagListResponse,
HealthCheckResponse,
N8nVersionInfo,
Variable,
WebhookRequest,
WorkflowExport,
@@ -24,6 +25,11 @@ import {
} from '../types/n8n-api';
import { handleN8nApiError, logN8nError } from '../utils/n8n-errors';
import { cleanWorkflowForCreate, cleanWorkflowForUpdate } from './n8n-validation';
import {
fetchN8nVersion,
cleanSettingsForVersion,
getCachedVersion,
} from './n8n-version';
export interface N8nApiClientConfig {
baseUrl: string;
@@ -35,15 +41,19 @@ export interface N8nApiClientConfig {
export class N8nApiClient {
private client: AxiosInstance;
private maxRetries: number;
private baseUrl: string;
private versionInfo: N8nVersionInfo | null = null;
private versionFetched = false;
constructor(config: N8nApiClientConfig) {
const { baseUrl, apiKey, timeout = 30000, maxRetries = 3 } = config;
this.maxRetries = maxRetries;
this.baseUrl = baseUrl;
// Ensure baseUrl ends with /api/v1
const apiUrl = baseUrl.endsWith('/api/v1')
? baseUrl
const apiUrl = baseUrl.endsWith('/api/v1')
? baseUrl
: `${baseUrl.replace(/\/$/, '')}/api/v1`;
this.client = axios.create({
@@ -84,25 +94,52 @@ 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
async healthCheck(): Promise<HealthCheckResponse> {
try {
// Try the standard healthz endpoint (available on all n8n instances)
const baseUrl = this.client.defaults.baseURL || '';
const healthzUrl = baseUrl.replace(/\/api\/v\d+\/?$/, '') + '/healthz';
const response = await axios.get(healthzUrl, {
timeout: 5000,
validateStatus: (status) => status < 500
});
// Also fetch version info (will be cached)
const versionInfo = await this.getVersion();
if (response.status === 200 && response.data?.status === 'ok') {
return {
return {
status: 'ok',
features: {} // Features detection would require additional endpoints
n8nVersion: versionInfo?.version,
features: {}
};
}
// If healthz doesn't work, fall back to API check
throw new Error('healthz endpoint not available');
} catch (error) {
@@ -110,8 +147,13 @@ export class N8nApiClient {
// This is a fallback for older n8n versions
try {
await this.client.get('/workflows', { params: { limit: 1 } });
return {
// Still try to get version
const versionInfo = await this.getVersion();
return {
status: 'ok',
n8nVersion: versionInfo?.version,
features: {}
};
} catch (fallbackError) {
@@ -142,8 +184,25 @@ export class N8nApiClient {
async updateWorkflow(id: string, workflow: Partial<Workflow>): Promise<Workflow> {
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);
// 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 {
const response = await this.client.put(`/workflows/${id}`, cleanedWorkflow);
return response.data;
@@ -288,7 +347,7 @@ export class N8nApiClient {
// Create a new axios instance for webhook requests to avoid API interceptors
const webhookClient = axios.create({
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);