feat: implement webhook path autofixer and improve node similarity service

- Add webhook path auto-generation for nodes missing path configuration
  - Generates UUID for both 'path' parameter and 'webhookId' field
  - Conditionally updates typeVersion to 2.1 only when < 2.1
  - High confidence fix (95%) as UUID generation is deterministic

- Fix critical security and performance issues in NodeSimilarityService:
  - Replace regex patterns with string-based matching to prevent ReDoS attacks
  - Add cache invalidation with version tracking to prevent memory leaks
  - Optimize Levenshtein distance algorithm from O(m*n) space to O(n)
  - Add early termination for performance improvement
  - Extract magic numbers into named constants

- Add comprehensive documentation for n8n_autofix_workflow tool
  - Document all fix types including new webhook-missing-path
  - Include examples, best practices, and warnings
  - Integrate with MCP tool documentation system

- Create node-type-utils for centralized type normalization
  - Eliminate code duplication across services
  - Consistent handling of package prefixes

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-09-24 11:18:13 +02:00
parent 627c0144a4
commit 0c31f12372
6 changed files with 697 additions and 123 deletions

View File

@@ -0,0 +1,143 @@
/**
* Utility functions for working with n8n node types
* Provides consistent normalization and transformation of node type strings
*/
/**
* Normalize a node type to the standard short form
* Handles both old-style (n8n-nodes-base.) and new-style (nodes-base.) prefixes
*
* @example
* normalizeNodeType('n8n-nodes-base.httpRequest') // 'nodes-base.httpRequest'
* normalizeNodeType('@n8n/n8n-nodes-langchain.openAi') // 'nodes-langchain.openAi'
* normalizeNodeType('nodes-base.webhook') // 'nodes-base.webhook' (unchanged)
*/
export function normalizeNodeType(type: string): string {
if (!type) return type;
return type
.replace(/^n8n-nodes-base\./, 'nodes-base.')
.replace(/^@n8n\/n8n-nodes-langchain\./, 'nodes-langchain.');
}
/**
* Convert a short-form node type to the full package name
*
* @example
* denormalizeNodeType('nodes-base.httpRequest', 'base') // 'n8n-nodes-base.httpRequest'
* denormalizeNodeType('nodes-langchain.openAi', 'langchain') // '@n8n/n8n-nodes-langchain.openAi'
*/
export function denormalizeNodeType(type: string, packageType: 'base' | 'langchain'): string {
if (!type) return type;
if (packageType === 'base') {
return type.replace(/^nodes-base\./, 'n8n-nodes-base.');
}
return type.replace(/^nodes-langchain\./, '@n8n/n8n-nodes-langchain.');
}
/**
* Extract the node name from a full node type
*
* @example
* extractNodeName('nodes-base.httpRequest') // 'httpRequest'
* extractNodeName('n8n-nodes-base.webhook') // 'webhook'
*/
export function extractNodeName(type: string): string {
if (!type) return '';
// First normalize the type
const normalized = normalizeNodeType(type);
// Extract everything after the last dot
const parts = normalized.split('.');
return parts[parts.length - 1] || '';
}
/**
* Get the package prefix from a node type
*
* @example
* getNodePackage('nodes-base.httpRequest') // 'nodes-base'
* getNodePackage('nodes-langchain.openAi') // 'nodes-langchain'
*/
export function getNodePackage(type: string): string | null {
if (!type || !type.includes('.')) return null;
// First normalize the type
const normalized = normalizeNodeType(type);
// Extract everything before the first dot
const parts = normalized.split('.');
return parts[0] || null;
}
/**
* Check if a node type is from the base package
*/
export function isBaseNode(type: string): boolean {
const normalized = normalizeNodeType(type);
return normalized.startsWith('nodes-base.');
}
/**
* Check if a node type is from the langchain package
*/
export function isLangChainNode(type: string): boolean {
const normalized = normalizeNodeType(type);
return normalized.startsWith('nodes-langchain.');
}
/**
* Validate if a string looks like a valid node type
* (has package prefix and node name)
*/
export function isValidNodeTypeFormat(type: string): boolean {
if (!type || typeof type !== 'string') return false;
// Must contain at least one dot
if (!type.includes('.')) return false;
const parts = type.split('.');
// Must have exactly 2 parts (package and node name)
if (parts.length !== 2) return false;
// Both parts must be non-empty
return parts[0].length > 0 && parts[1].length > 0;
}
/**
* Try multiple variations of a node type to find a match
* Returns an array of variations to try in order
*
* @example
* getNodeTypeVariations('httpRequest')
* // ['nodes-base.httpRequest', 'n8n-nodes-base.httpRequest', 'nodes-langchain.httpRequest', ...]
*/
export function getNodeTypeVariations(type: string): string[] {
const variations: string[] = [];
// If it already has a package prefix, try normalized version first
if (type.includes('.')) {
variations.push(normalizeNodeType(type));
// Also try the denormalized versions
const normalized = normalizeNodeType(type);
if (normalized.startsWith('nodes-base.')) {
variations.push(denormalizeNodeType(normalized, 'base'));
} else if (normalized.startsWith('nodes-langchain.')) {
variations.push(denormalizeNodeType(normalized, 'langchain'));
}
} else {
// No package prefix, try common packages
variations.push(`nodes-base.${type}`);
variations.push(`n8n-nodes-base.${type}`);
variations.push(`nodes-langchain.${type}`);
variations.push(`@n8n/n8n-nodes-langchain.${type}`);
}
// Remove duplicates while preserving order
return [...new Set(variations)];
}