Spaces:
Sleeping
Sleeping
| /** | |
| * 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()); | |
| } | |
| } | |