mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 06:22:04 +00:00
231 lines
8.8 KiB
JavaScript
231 lines
8.8 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.NodeMigrationService = void 0;
|
|
const uuid_1 = require("uuid");
|
|
class NodeMigrationService {
|
|
constructor(versionService, breakingChangeDetector) {
|
|
this.versionService = versionService;
|
|
this.breakingChangeDetector = breakingChangeDetector;
|
|
}
|
|
async migrateNode(node, fromVersion, toVersion) {
|
|
const nodeId = node.id || 'unknown';
|
|
const nodeName = node.name || 'Unknown Node';
|
|
const nodeType = node.type;
|
|
const analysis = await this.breakingChangeDetector.analyzeVersionUpgrade(nodeType, fromVersion, toVersion);
|
|
const migratedNode = JSON.parse(JSON.stringify(node));
|
|
migratedNode.typeVersion = this.parseVersion(toVersion);
|
|
const appliedMigrations = [];
|
|
const remainingIssues = [];
|
|
for (const change of analysis.changes.filter(c => c.autoMigratable)) {
|
|
const migration = this.applyMigration(migratedNode, change);
|
|
if (migration) {
|
|
appliedMigrations.push(migration);
|
|
}
|
|
}
|
|
for (const change of analysis.changes.filter(c => !c.autoMigratable)) {
|
|
remainingIssues.push(`Manual action required for "${change.propertyName}": ${change.migrationHint}`);
|
|
}
|
|
let confidence = 'HIGH';
|
|
if (remainingIssues.length > 0) {
|
|
confidence = remainingIssues.length > 3 ? 'LOW' : 'MEDIUM';
|
|
}
|
|
return {
|
|
success: remainingIssues.length === 0,
|
|
nodeId,
|
|
nodeName,
|
|
fromVersion,
|
|
toVersion,
|
|
appliedMigrations,
|
|
remainingIssues,
|
|
confidence,
|
|
updatedNode: migratedNode
|
|
};
|
|
}
|
|
applyMigration(node, change) {
|
|
if (!change.migrationStrategy)
|
|
return null;
|
|
const { type, defaultValue, sourceProperty, targetProperty } = change.migrationStrategy;
|
|
switch (type) {
|
|
case 'add_property':
|
|
return this.addProperty(node, change.propertyName, defaultValue, change);
|
|
case 'remove_property':
|
|
return this.removeProperty(node, change.propertyName, change);
|
|
case 'rename_property':
|
|
return this.renameProperty(node, sourceProperty, targetProperty, change);
|
|
case 'set_default':
|
|
return this.setDefault(node, change.propertyName, defaultValue, change);
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
addProperty(node, propertyPath, defaultValue, change) {
|
|
const value = this.resolveDefaultValue(propertyPath, defaultValue, node);
|
|
const parts = propertyPath.split('.');
|
|
let target = node;
|
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
const part = parts[i];
|
|
if (!target[part]) {
|
|
target[part] = {};
|
|
}
|
|
target = target[part];
|
|
}
|
|
const finalKey = parts[parts.length - 1];
|
|
target[finalKey] = value;
|
|
return {
|
|
propertyName: propertyPath,
|
|
action: 'Added property',
|
|
newValue: value,
|
|
description: `Added "${propertyPath}" with default value`
|
|
};
|
|
}
|
|
removeProperty(node, propertyPath, change) {
|
|
const parts = propertyPath.split('.');
|
|
let target = node;
|
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
const part = parts[i];
|
|
if (!target[part])
|
|
return null;
|
|
target = target[part];
|
|
}
|
|
const finalKey = parts[parts.length - 1];
|
|
const oldValue = target[finalKey];
|
|
if (oldValue !== undefined) {
|
|
delete target[finalKey];
|
|
return {
|
|
propertyName: propertyPath,
|
|
action: 'Removed property',
|
|
oldValue,
|
|
description: `Removed deprecated property "${propertyPath}"`
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
renameProperty(node, sourcePath, targetPath, change) {
|
|
const sourceParts = sourcePath.split('.');
|
|
let sourceTarget = node;
|
|
for (let i = 0; i < sourceParts.length - 1; i++) {
|
|
if (!sourceTarget[sourceParts[i]])
|
|
return null;
|
|
sourceTarget = sourceTarget[sourceParts[i]];
|
|
}
|
|
const sourceKey = sourceParts[sourceParts.length - 1];
|
|
const oldValue = sourceTarget[sourceKey];
|
|
if (oldValue === undefined)
|
|
return null;
|
|
const targetParts = targetPath.split('.');
|
|
let targetTarget = node;
|
|
for (let i = 0; i < targetParts.length - 1; i++) {
|
|
if (!targetTarget[targetParts[i]]) {
|
|
targetTarget[targetParts[i]] = {};
|
|
}
|
|
targetTarget = targetTarget[targetParts[i]];
|
|
}
|
|
const targetKey = targetParts[targetParts.length - 1];
|
|
targetTarget[targetKey] = oldValue;
|
|
delete sourceTarget[sourceKey];
|
|
return {
|
|
propertyName: targetPath,
|
|
action: 'Renamed property',
|
|
oldValue: `${sourcePath}: ${JSON.stringify(oldValue)}`,
|
|
newValue: `${targetPath}: ${JSON.stringify(oldValue)}`,
|
|
description: `Renamed "${sourcePath}" to "${targetPath}"`
|
|
};
|
|
}
|
|
setDefault(node, propertyPath, defaultValue, change) {
|
|
const parts = propertyPath.split('.');
|
|
let target = node;
|
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
if (!target[parts[i]]) {
|
|
target[parts[i]] = {};
|
|
}
|
|
target = target[parts[i]];
|
|
}
|
|
const finalKey = parts[parts.length - 1];
|
|
if (target[finalKey] === undefined) {
|
|
const value = this.resolveDefaultValue(propertyPath, defaultValue, node);
|
|
target[finalKey] = value;
|
|
return {
|
|
propertyName: propertyPath,
|
|
action: 'Set default value',
|
|
newValue: value,
|
|
description: `Set default value for "${propertyPath}"`
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
resolveDefaultValue(propertyPath, defaultValue, node) {
|
|
if (propertyPath === 'webhookId' || propertyPath.endsWith('.webhookId')) {
|
|
return (0, uuid_1.v4)();
|
|
}
|
|
if (propertyPath === 'path' || propertyPath.endsWith('.path')) {
|
|
if (node.type === 'n8n-nodes-base.webhook') {
|
|
return `/webhook-${Date.now()}`;
|
|
}
|
|
}
|
|
return defaultValue !== null && defaultValue !== undefined ? defaultValue : null;
|
|
}
|
|
parseVersion(version) {
|
|
const parts = version.split('.').map(Number);
|
|
if (parts.length === 1)
|
|
return parts[0];
|
|
if (parts.length === 2)
|
|
return parts[0] + parts[1] / 10;
|
|
return parts[0];
|
|
}
|
|
async validateMigratedNode(node, nodeType) {
|
|
const errors = [];
|
|
const warnings = [];
|
|
if (!node.typeVersion) {
|
|
errors.push('Missing typeVersion after migration');
|
|
}
|
|
if (!node.parameters) {
|
|
errors.push('Missing parameters object');
|
|
}
|
|
if (nodeType === 'n8n-nodes-base.webhook') {
|
|
if (!node.parameters?.path) {
|
|
errors.push('Webhook node missing required "path" parameter');
|
|
}
|
|
if (node.typeVersion >= 2.1 && !node.webhookId) {
|
|
warnings.push('Webhook v2.1+ typically requires webhookId');
|
|
}
|
|
}
|
|
if (nodeType === 'n8n-nodes-base.executeWorkflow') {
|
|
if (node.typeVersion >= 1.1 && !node.parameters?.inputFieldMapping) {
|
|
errors.push('Execute Workflow v1.1+ requires inputFieldMapping');
|
|
}
|
|
}
|
|
return {
|
|
valid: errors.length === 0,
|
|
errors,
|
|
warnings
|
|
};
|
|
}
|
|
async migrateWorkflowNodes(workflow, targetVersions) {
|
|
const results = [];
|
|
for (const node of workflow.nodes || []) {
|
|
const targetVersion = targetVersions[node.id];
|
|
if (targetVersion && node.typeVersion) {
|
|
const currentVersion = node.typeVersion.toString();
|
|
const result = await this.migrateNode(node, currentVersion, targetVersion);
|
|
results.push(result);
|
|
Object.assign(node, result.updatedNode);
|
|
}
|
|
}
|
|
const confidences = results.map(r => r.confidence);
|
|
let overallConfidence = 'HIGH';
|
|
if (confidences.includes('LOW')) {
|
|
overallConfidence = 'LOW';
|
|
}
|
|
else if (confidences.includes('MEDIUM')) {
|
|
overallConfidence = 'MEDIUM';
|
|
}
|
|
const success = results.every(r => r.success);
|
|
return {
|
|
success,
|
|
results,
|
|
overallConfidence
|
|
};
|
|
}
|
|
}
|
|
exports.NodeMigrationService = NodeMigrationService;
|
|
//# sourceMappingURL=node-migration-service.js.map
|