n8n-workflow-manager
Version:
Command-line tools for managing n8n workflows with version control and TypeScript support
265 lines (227 loc) • 7.45 kB
JavaScript
const fs = require('fs');
const path = require('path');
const ConfigManager = require('../utils/config');
/**
* Extract n8n workflow into individual files for better version control
*/
class WorkflowExtractor {
constructor(configManager = null) {
this.configManager = configManager || new ConfigManager();
this.config = this.configManager.config;
this.paths = this.configManager.getPaths();
}
/**
* Get file extension based on node type
*/
getNodeFileExtension(nodeType, parameters) {
switch (nodeType) {
case 'n8n-nodes-base.code': {
// Check if code contains TypeScript patterns
const jsCode = parameters?.jsCode || '';
if (jsCode.includes('interface ') || jsCode.includes('type ') || jsCode.includes(': string') || jsCode.includes(': number')) {
return '.ts';
}
return '.js';
}
case 'n8n-nodes-base.webhook':
case 'n8n-nodes-base.httpRequest':
case 'n8n-nodes-base.if':
case 'n8n-nodes-base.github':
case 'n8n-nodes-base.linearTrigger':
return '.json';
default:
return '.json';
}
}
/**
* Sanitize node name for filesystem
*/
sanitizeNodeName(nodeName) {
return nodeName
.toLowerCase()
.replace(/[^a-z0-9]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
}
/**
* Extract code content from code nodes
*/
extractCodeNode(node) {
const { jsCode, ...otherParams } = node.parameters || {};
return {
codeContent: jsCode || '',
nodeConfig: {
...node,
parameters: {
...otherParams,
jsCode: '// CODE_PLACEHOLDER - content loaded from external file'
}
}
};
}
/**
* Extract single workflow
*/
extractWorkflow(workflowName) {
const workflowPath = path.join(this.paths.n8nDir, `${workflowName}.json`);
if (!fs.existsSync(workflowPath)) {
throw new Error(`Workflow file not found: ${workflowPath}`);
}
console.log(`🔍 Extracting workflow: ${workflowName}`);
const workflow = JSON.parse(fs.readFileSync(workflowPath, 'utf-8'));
const targetDir = path.join(this.paths.workflowsDir, workflowName);
const nodesDir = path.join(targetDir, 'nodes');
const typesDir = path.join(targetDir, 'types');
// Create directories
if (!fs.existsSync(nodesDir)) {
fs.mkdirSync(nodesDir, { recursive: true });
}
if (!fs.existsSync(typesDir)) {
fs.mkdirSync(typesDir, { recursive: true });
}
// Extract metadata
const metadata = {
name: workflow.name,
active: workflow.active,
settings: workflow.settings,
versionId: workflow.versionId,
meta: workflow.meta,
id: this.config.extraction.preserveIds ? workflow.id : undefined,
tags: workflow.tags,
extractedAt: new Date().toISOString(),
nodeCount: workflow.nodes?.length || 0
};
// Remove undefined values
Object.keys(metadata).forEach(key => {
if (metadata[key] === undefined) {
delete metadata[key];
}
});
fs.writeFileSync(
path.join(targetDir, 'metadata.json'),
JSON.stringify(metadata, null, 2)
);
// Extract connections
if (workflow.connections) {
fs.writeFileSync(
path.join(targetDir, 'connections.json'),
JSON.stringify(workflow.connections, null, 2)
);
}
// Extract nodes
const nodeIndex = {};
const extractedNodes = [];
if (workflow.nodes) {
workflow.nodes.forEach((node, index) => {
const sanitizedName = this.sanitizeNodeName(node.name);
const extension = this.getNodeFileExtension(node.type, node.parameters);
const filename = `${sanitizedName}${extension}`;
console.log(` 📄 Extracting node: ${node.name} -> ${filename}`);
if (this.config.extraction.generateIndex) {
nodeIndex[node.id] = {
name: node.name,
filename: filename,
type: node.type,
originalIndex: index
};
}
if (node.type === 'n8n-nodes-base.code' && this.config.extraction.splitCodeNodes) {
const { codeContent, nodeConfig } = this.extractCodeNode(node);
// Save code to separate file
fs.writeFileSync(path.join(nodesDir, filename), codeContent);
// Save node configuration
const configFilename = `${sanitizedName}.config.json`;
fs.writeFileSync(
path.join(nodesDir, configFilename),
JSON.stringify(nodeConfig, null, 2)
);
extractedNodes.push({
...nodeConfig,
_extractedCodeFile: filename,
_extractedConfigFile: configFilename
});
} else {
// Save regular node
fs.writeFileSync(
path.join(nodesDir, filename),
JSON.stringify(node, null, 2)
);
extractedNodes.push({
...node,
_extractedFile: filename
});
}
});
}
// Save node index if enabled
if (this.config.extraction.generateIndex) {
fs.writeFileSync(
path.join(targetDir, 'node-index.json'),
JSON.stringify(nodeIndex, null, 2)
);
}
// Create main workflow file with references
const extractedWorkflow = {
...workflow,
nodes: extractedNodes,
_extracted: true,
_extractedAt: new Date().toISOString(),
_extractedWith: 'n8n-workflow-manager'
};
fs.writeFileSync(
path.join(targetDir, 'workflow.json'),
JSON.stringify(extractedWorkflow, null, 2)
);
console.log(`✅ Workflow extracted to: ${targetDir}`);
console.log(` 📊 ${workflow.nodes?.length || 0} nodes extracted`);
console.log(` 📁 ${Object.keys(nodeIndex).length} files created`);
return {
targetDir,
nodeCount: workflow.nodes?.length || 0,
extractedFiles: Object.values(nodeIndex).map(n => n.filename)
};
}
/**
* Extract all workflows
*/
extractAll() {
const workflowFiles = this.configManager.getWorkflowFiles();
console.log(`🔍 Found ${workflowFiles.length} workflow(s) to extract`);
const results = [];
for (const workflowName of workflowFiles) {
try {
const result = this.extractWorkflow(workflowName);
results.push({ workflowName, success: true, ...result });
} catch (error) {
console.error(`❌ Failed to extract ${workflowName}:`, error.message);
results.push({ workflowName, success: false, error: error.message });
}
}
return results;
}
/**
* Get extraction statistics
*/
getStats() {
const workflowFiles = this.configManager.getWorkflowFiles();
const extractedWorkflows = this.configManager.getExtractedWorkflows();
return {
totalWorkflows: workflowFiles.length,
extractedWorkflowsCount: extractedWorkflows.length,
workflowFiles,
extractedWorkflows,
paths: this.paths
};
}
/**
* Validate extraction environment
*/
validateEnvironment() {
const validation = this.configManager.validate();
if (!validation.valid) {
throw new Error(`Environment validation failed: ${validation.errors.join(', ')}`);
}
return validation;
}
}
module.exports = WorkflowExtractor;