mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 22:42:04 +00:00
Implemented comprehensive node version upgrade functionality with intelligent migration and breaking change detection. Key Features: - Smart version upgrades (typeversion-upgrade fix type) - Version migration guidance (version-migration fix type) - Auto-migration for Execute Workflow v1.0→v1.1 (adds inputFieldMapping) - Auto-migration for Webhook v2.0→v2.1 (generates webhookId) - Breaking changes registry with extensible patterns - AI-friendly post-update validation guidance - Confidence-based application (HIGH/MEDIUM/LOW) Architecture: - NodeVersionService: Version discovery and comparison - BreakingChangeDetector: Registry + dynamic schema comparison - NodeMigrationService: Smart property migrations - PostUpdateValidator: Step-by-step migration instructions - Enhanced database schema: node_versions, version_property_changes tables Services Created: - src/services/breaking-changes-registry.ts - src/services/breaking-change-detector.ts - src/services/node-version-service.ts - src/services/node-migration-service.ts - src/services/post-update-validator.ts Database Enhanced: - src/database/schema.sql (new version tracking tables) - src/database/node-repository.ts (15+ version query methods) Autofixer Integration: - src/services/workflow-auto-fixer.ts (async, new fix types) - src/mcp/handlers-n8n-manager.ts (await generateFixes) - src/mcp/tools-n8n-manager.ts (schema with new fix types) Documentation: - src/mcp/tool-docs/workflow_management/n8n-autofix-workflow.ts - CHANGELOG.md (comprehensive feature documentation) Testing: - Fixed all test scripts to await async generateFixes() - Added test workflow for Execute Workflow v1.0 upgrade testing Bug Fixes: - Fixed MCP tool schema enum to include new fix types - Fixed confidence type mapping (lowercase → uppercase) Conceived by Romuald Członkowski - www.aiadvisors.pl/en
378 lines
10 KiB
TypeScript
378 lines
10 KiB
TypeScript
/**
|
|
* Node Version Service
|
|
*
|
|
* Central service for node version discovery, comparison, and upgrade path recommendation.
|
|
* Provides caching for performance and integrates with the database and breaking change detector.
|
|
*/
|
|
|
|
import { NodeRepository } from '../database/node-repository';
|
|
import { BreakingChangeDetector } from './breaking-change-detector';
|
|
|
|
export interface NodeVersion {
|
|
nodeType: string;
|
|
version: string;
|
|
packageName: string;
|
|
displayName: string;
|
|
isCurrentMax: boolean;
|
|
minimumN8nVersion?: string;
|
|
breakingChanges: any[];
|
|
deprecatedProperties: string[];
|
|
addedProperties: string[];
|
|
releasedAt?: Date;
|
|
}
|
|
|
|
export interface VersionComparison {
|
|
nodeType: string;
|
|
currentVersion: string;
|
|
latestVersion: string;
|
|
isOutdated: boolean;
|
|
versionGap: number; // How many versions behind
|
|
hasBreakingChanges: boolean;
|
|
recommendUpgrade: boolean;
|
|
confidence: 'HIGH' | 'MEDIUM' | 'LOW';
|
|
reason: string;
|
|
}
|
|
|
|
export interface UpgradePath {
|
|
nodeType: string;
|
|
fromVersion: string;
|
|
toVersion: string;
|
|
direct: boolean; // Can upgrade directly or needs intermediate steps
|
|
intermediateVersions: string[]; // If multi-step upgrade needed
|
|
totalBreakingChanges: number;
|
|
autoMigratableChanges: number;
|
|
manualRequiredChanges: number;
|
|
estimatedEffort: 'LOW' | 'MEDIUM' | 'HIGH';
|
|
steps: UpgradeStep[];
|
|
}
|
|
|
|
export interface UpgradeStep {
|
|
fromVersion: string;
|
|
toVersion: string;
|
|
breakingChanges: number;
|
|
migrationHints: string[];
|
|
}
|
|
|
|
/**
|
|
* Node Version Service with caching
|
|
*/
|
|
export class NodeVersionService {
|
|
private versionCache: Map<string, NodeVersion[]> = new Map();
|
|
private cacheTTL: number = 5 * 60 * 1000; // 5 minutes
|
|
private cacheTimestamps: Map<string, number> = new Map();
|
|
|
|
constructor(
|
|
private nodeRepository: NodeRepository,
|
|
private breakingChangeDetector: BreakingChangeDetector
|
|
) {}
|
|
|
|
/**
|
|
* Get all available versions for a node type
|
|
*/
|
|
getAvailableVersions(nodeType: string): NodeVersion[] {
|
|
// Check cache first
|
|
const cached = this.getCachedVersions(nodeType);
|
|
if (cached) return cached;
|
|
|
|
// Query from database
|
|
const versions = this.nodeRepository.getNodeVersions(nodeType);
|
|
|
|
// Cache the result
|
|
this.cacheVersions(nodeType, versions);
|
|
|
|
return versions;
|
|
}
|
|
|
|
/**
|
|
* Get the latest available version for a node type
|
|
*/
|
|
getLatestVersion(nodeType: string): string | null {
|
|
const versions = this.getAvailableVersions(nodeType);
|
|
|
|
if (versions.length === 0) {
|
|
// Fallback to main nodes table
|
|
const node = this.nodeRepository.getNode(nodeType);
|
|
return node?.version || null;
|
|
}
|
|
|
|
// Find version marked as current max
|
|
const maxVersion = versions.find(v => v.isCurrentMax);
|
|
if (maxVersion) return maxVersion.version;
|
|
|
|
// Fallback: sort and get highest
|
|
const sorted = versions.sort((a, b) => this.compareVersions(b.version, a.version));
|
|
return sorted[0]?.version || null;
|
|
}
|
|
|
|
/**
|
|
* Compare a node's current version against the latest available
|
|
*/
|
|
compareVersions(currentVersion: string, latestVersion: string): number {
|
|
const parts1 = currentVersion.split('.').map(Number);
|
|
const parts2 = latestVersion.split('.').map(Number);
|
|
|
|
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
const p1 = parts1[i] || 0;
|
|
const p2 = parts2[i] || 0;
|
|
|
|
if (p1 < p2) return -1;
|
|
if (p1 > p2) return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Analyze if a node version is outdated and should be upgraded
|
|
*/
|
|
analyzeVersion(nodeType: string, currentVersion: string): VersionComparison {
|
|
const latestVersion = this.getLatestVersion(nodeType);
|
|
|
|
if (!latestVersion) {
|
|
return {
|
|
nodeType,
|
|
currentVersion,
|
|
latestVersion: currentVersion,
|
|
isOutdated: false,
|
|
versionGap: 0,
|
|
hasBreakingChanges: false,
|
|
recommendUpgrade: false,
|
|
confidence: 'HIGH',
|
|
reason: 'No version information available. Using current version.'
|
|
};
|
|
}
|
|
|
|
const comparison = this.compareVersions(currentVersion, latestVersion);
|
|
const isOutdated = comparison < 0;
|
|
|
|
if (!isOutdated) {
|
|
return {
|
|
nodeType,
|
|
currentVersion,
|
|
latestVersion,
|
|
isOutdated: false,
|
|
versionGap: 0,
|
|
hasBreakingChanges: false,
|
|
recommendUpgrade: false,
|
|
confidence: 'HIGH',
|
|
reason: 'Node is already at the latest version.'
|
|
};
|
|
}
|
|
|
|
// Calculate version gap
|
|
const versionGap = this.calculateVersionGap(currentVersion, latestVersion);
|
|
|
|
// Check for breaking changes
|
|
const hasBreakingChanges = this.breakingChangeDetector.hasBreakingChanges(
|
|
nodeType,
|
|
currentVersion,
|
|
latestVersion
|
|
);
|
|
|
|
// Determine upgrade recommendation and confidence
|
|
let recommendUpgrade = true;
|
|
let confidence: 'HIGH' | 'MEDIUM' | 'LOW' = 'HIGH';
|
|
let reason = `Version ${latestVersion} available. `;
|
|
|
|
if (hasBreakingChanges) {
|
|
confidence = 'MEDIUM';
|
|
reason += 'Contains breaking changes. Review before upgrading.';
|
|
} else {
|
|
reason += 'Safe to upgrade (no breaking changes detected).';
|
|
}
|
|
|
|
if (versionGap > 2) {
|
|
confidence = 'LOW';
|
|
reason += ` Version gap is large (${versionGap} versions). Consider incremental upgrade.`;
|
|
}
|
|
|
|
return {
|
|
nodeType,
|
|
currentVersion,
|
|
latestVersion,
|
|
isOutdated,
|
|
versionGap,
|
|
hasBreakingChanges,
|
|
recommendUpgrade,
|
|
confidence,
|
|
reason
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Calculate the version gap (number of versions between)
|
|
*/
|
|
private calculateVersionGap(fromVersion: string, toVersion: string): number {
|
|
const from = fromVersion.split('.').map(Number);
|
|
const to = toVersion.split('.').map(Number);
|
|
|
|
// Simple gap calculation based on version numbers
|
|
let gap = 0;
|
|
|
|
for (let i = 0; i < Math.max(from.length, to.length); i++) {
|
|
const f = from[i] || 0;
|
|
const t = to[i] || 0;
|
|
gap += Math.abs(t - f);
|
|
}
|
|
|
|
return gap;
|
|
}
|
|
|
|
/**
|
|
* Suggest the best upgrade path for a node
|
|
*/
|
|
async suggestUpgradePath(nodeType: string, currentVersion: string): Promise<UpgradePath | null> {
|
|
const latestVersion = this.getLatestVersion(nodeType);
|
|
|
|
if (!latestVersion) return null;
|
|
|
|
const comparison = this.compareVersions(currentVersion, latestVersion);
|
|
if (comparison >= 0) return null; // Already at latest or newer
|
|
|
|
// Get all available versions between current and latest
|
|
const allVersions = this.getAvailableVersions(nodeType);
|
|
const intermediateVersions = allVersions
|
|
.filter(v =>
|
|
this.compareVersions(v.version, currentVersion) > 0 &&
|
|
this.compareVersions(v.version, latestVersion) < 0
|
|
)
|
|
.map(v => v.version)
|
|
.sort((a, b) => this.compareVersions(a, b));
|
|
|
|
// Analyze the upgrade
|
|
const analysis = await this.breakingChangeDetector.analyzeVersionUpgrade(
|
|
nodeType,
|
|
currentVersion,
|
|
latestVersion
|
|
);
|
|
|
|
// Determine if direct upgrade is safe
|
|
const versionGap = this.calculateVersionGap(currentVersion, latestVersion);
|
|
const direct = versionGap <= 1 || !analysis.hasBreakingChanges;
|
|
|
|
// Generate upgrade steps
|
|
const steps: UpgradeStep[] = [];
|
|
|
|
if (direct || intermediateVersions.length === 0) {
|
|
// Direct upgrade
|
|
steps.push({
|
|
fromVersion: currentVersion,
|
|
toVersion: latestVersion,
|
|
breakingChanges: analysis.changes.filter(c => c.isBreaking).length,
|
|
migrationHints: analysis.recommendations
|
|
});
|
|
} else {
|
|
// Multi-step upgrade through intermediate versions
|
|
let stepFrom = currentVersion;
|
|
|
|
for (const intermediateVersion of intermediateVersions) {
|
|
const stepAnalysis = await this.breakingChangeDetector.analyzeVersionUpgrade(
|
|
nodeType,
|
|
stepFrom,
|
|
intermediateVersion
|
|
);
|
|
|
|
steps.push({
|
|
fromVersion: stepFrom,
|
|
toVersion: intermediateVersion,
|
|
breakingChanges: stepAnalysis.changes.filter(c => c.isBreaking).length,
|
|
migrationHints: stepAnalysis.recommendations
|
|
});
|
|
|
|
stepFrom = intermediateVersion;
|
|
}
|
|
|
|
// Final step to latest
|
|
const finalStepAnalysis = await this.breakingChangeDetector.analyzeVersionUpgrade(
|
|
nodeType,
|
|
stepFrom,
|
|
latestVersion
|
|
);
|
|
|
|
steps.push({
|
|
fromVersion: stepFrom,
|
|
toVersion: latestVersion,
|
|
breakingChanges: finalStepAnalysis.changes.filter(c => c.isBreaking).length,
|
|
migrationHints: finalStepAnalysis.recommendations
|
|
});
|
|
}
|
|
|
|
// Calculate estimated effort
|
|
const totalBreakingChanges = steps.reduce((sum, step) => sum + step.breakingChanges, 0);
|
|
let estimatedEffort: 'LOW' | 'MEDIUM' | 'HIGH' = 'LOW';
|
|
|
|
if (totalBreakingChanges > 5 || steps.length > 3) {
|
|
estimatedEffort = 'HIGH';
|
|
} else if (totalBreakingChanges > 2 || steps.length > 1) {
|
|
estimatedEffort = 'MEDIUM';
|
|
}
|
|
|
|
return {
|
|
nodeType,
|
|
fromVersion: currentVersion,
|
|
toVersion: latestVersion,
|
|
direct,
|
|
intermediateVersions,
|
|
totalBreakingChanges,
|
|
autoMigratableChanges: analysis.autoMigratableCount,
|
|
manualRequiredChanges: analysis.manualRequiredCount,
|
|
estimatedEffort,
|
|
steps
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if a specific version exists for a node
|
|
*/
|
|
versionExists(nodeType: string, version: string): boolean {
|
|
const versions = this.getAvailableVersions(nodeType);
|
|
return versions.some(v => v.version === version);
|
|
}
|
|
|
|
/**
|
|
* Get version metadata (breaking changes, added/deprecated properties)
|
|
*/
|
|
getVersionMetadata(nodeType: string, version: string): NodeVersion | null {
|
|
const versionData = this.nodeRepository.getNodeVersion(nodeType, version);
|
|
return versionData;
|
|
}
|
|
|
|
/**
|
|
* Clear the version cache
|
|
*/
|
|
clearCache(nodeType?: string): void {
|
|
if (nodeType) {
|
|
this.versionCache.delete(nodeType);
|
|
this.cacheTimestamps.delete(nodeType);
|
|
} else {
|
|
this.versionCache.clear();
|
|
this.cacheTimestamps.clear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get cached versions if still valid
|
|
*/
|
|
private getCachedVersions(nodeType: string): NodeVersion[] | null {
|
|
const cached = this.versionCache.get(nodeType);
|
|
const timestamp = this.cacheTimestamps.get(nodeType);
|
|
|
|
if (cached && timestamp) {
|
|
const age = Date.now() - timestamp;
|
|
if (age < this.cacheTTL) {
|
|
return cached;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Cache versions with timestamp
|
|
*/
|
|
private cacheVersions(nodeType: string, versions: NodeVersion[]): void {
|
|
this.versionCache.set(nodeType, versions);
|
|
this.cacheTimestamps.set(nodeType, Date.now());
|
|
}
|
|
}
|