chore: add pre-built dist folder for npx usage

This commit is contained in:
thesved
2025-12-04 20:22:02 +02:00
committed by Romuald Członkowski
parent a70d96a373
commit 5057481e70
716 changed files with 48021 additions and 0 deletions

12
dist/services/ai-node-validator.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
import { WorkflowNode, WorkflowJson, ReverseConnection, ValidationIssue } from './ai-tool-validators';
export type { WorkflowNode, WorkflowJson, ReverseConnection, ValidationIssue } from './ai-tool-validators';
export declare const AI_CONNECTION_TYPES: readonly ["ai_languageModel", "ai_memory", "ai_tool", "ai_embedding", "ai_vectorStore", "ai_document", "ai_textSplitter", "ai_outputParser"];
export declare function buildReverseConnectionMap(workflow: WorkflowJson): Map<string, ReverseConnection[]>;
export declare function getAIConnections(nodeName: string, reverseConnections: Map<string, ReverseConnection[]>, connectionType?: string): ReverseConnection[];
export declare function validateAIAgent(node: WorkflowNode, reverseConnections: Map<string, ReverseConnection[]>, workflow: WorkflowJson): ValidationIssue[];
export declare function validateChatTrigger(node: WorkflowNode, workflow: WorkflowJson, reverseConnections: Map<string, ReverseConnection[]>): ValidationIssue[];
export declare function validateBasicLLMChain(node: WorkflowNode, reverseConnections: Map<string, ReverseConnection[]>): ValidationIssue[];
export declare function validateAISpecificNodes(workflow: WorkflowJson): ValidationIssue[];
export declare function hasAINodes(workflow: WorkflowJson): boolean;
export declare function getAINodeCategory(nodeType: string): string | null;
//# sourceMappingURL=ai-node-validator.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ai-node-validator.d.ts","sourceRoot":"","sources":["../../src/services/ai-node-validator.ts"],"names":[],"mappings":"AAcA,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,eAAe,EAGhB,MAAM,sBAAsB,CAAC;AAG9B,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,eAAe,EAChB,MAAM,sBAAsB,CAAC;AAU9B,eAAO,MAAM,mBAAmB,8IAStB,CAAC;AAmBX,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,YAAY,GACrB,GAAG,CAAC,MAAM,EAAE,iBAAiB,EAAE,CAAC,CA4ClC;AAKD,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,kBAAkB,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,EAAE,CAAC,EACpD,cAAc,CAAC,EAAE,MAAM,GACtB,iBAAiB,EAAE,CAQrB;AAgBD,wBAAgB,eAAe,CAC7B,IAAI,EAAE,YAAY,EAClB,kBAAkB,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,EAAE,CAAC,EACpD,QAAQ,EAAE,YAAY,GACrB,eAAe,EAAE,CAqLnB;AAyCD,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,YAAY,EAClB,QAAQ,EAAE,YAAY,EACtB,kBAAkB,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,EAAE,CAAC,GACnD,eAAe,EAAE,CA8EnB;AAYD,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,YAAY,EAClB,kBAAkB,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,EAAE,CAAC,GACnD,eAAe,EAAE,CAiEnB;AAOD,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,YAAY,GACrB,eAAe,EAAE,CA0CnB;AAMD,wBAAgB,UAAU,CAAC,QAAQ,EAAE,YAAY,GAAG,OAAO,CAW1D;AAKD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA0BjE"}

429
dist/services/ai-node-validator.js vendored Normal file
View File

@@ -0,0 +1,429 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AI_CONNECTION_TYPES = void 0;
exports.buildReverseConnectionMap = buildReverseConnectionMap;
exports.getAIConnections = getAIConnections;
exports.validateAIAgent = validateAIAgent;
exports.validateChatTrigger = validateChatTrigger;
exports.validateBasicLLMChain = validateBasicLLMChain;
exports.validateAISpecificNodes = validateAISpecificNodes;
exports.hasAINodes = hasAINodes;
exports.getAINodeCategory = getAINodeCategory;
const node_type_normalizer_1 = require("../utils/node-type-normalizer");
const ai_tool_validators_1 = require("./ai-tool-validators");
const MIN_SYSTEM_MESSAGE_LENGTH = 20;
const MAX_ITERATIONS_WARNING_THRESHOLD = 50;
exports.AI_CONNECTION_TYPES = [
'ai_languageModel',
'ai_memory',
'ai_tool',
'ai_embedding',
'ai_vectorStore',
'ai_document',
'ai_textSplitter',
'ai_outputParser'
];
function buildReverseConnectionMap(workflow) {
const map = new Map();
for (const [sourceName, outputs] of Object.entries(workflow.connections)) {
if (!sourceName || typeof sourceName !== 'string' || sourceName.trim() === '') {
continue;
}
if (!outputs || typeof outputs !== 'object')
continue;
for (const [outputType, connections] of Object.entries(outputs)) {
if (!Array.isArray(connections))
continue;
const connArray = connections.flat().filter(c => c);
for (const conn of connArray) {
if (!conn || !conn.node)
continue;
if (typeof conn.node !== 'string' || conn.node.trim() === '') {
continue;
}
if (!map.has(conn.node)) {
map.set(conn.node, []);
}
map.get(conn.node).push({
sourceName: sourceName,
sourceType: outputType,
type: outputType,
index: conn.index ?? 0
});
}
}
}
return map;
}
function getAIConnections(nodeName, reverseConnections, connectionType) {
const incoming = reverseConnections.get(nodeName) || [];
if (connectionType) {
return incoming.filter(c => c.type === connectionType);
}
return incoming.filter(c => exports.AI_CONNECTION_TYPES.includes(c.type));
}
function validateAIAgent(node, reverseConnections, workflow) {
const issues = [];
const incoming = reverseConnections.get(node.name) || [];
const languageModelConnections = incoming.filter(c => c.type === 'ai_languageModel');
if (languageModelConnections.length === 0) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `AI Agent "${node.name}" requires an ai_languageModel connection. Connect a language model node (e.g., OpenAI Chat Model, Anthropic Chat Model).`,
code: 'MISSING_LANGUAGE_MODEL'
});
}
else if (languageModelConnections.length > 2) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `AI Agent "${node.name}" has ${languageModelConnections.length} ai_languageModel connections. Maximum is 2 (for fallback model support).`,
code: 'TOO_MANY_LANGUAGE_MODELS'
});
}
else if (languageModelConnections.length === 2) {
if (!node.parameters.needsFallback) {
issues.push({
severity: 'warning',
nodeId: node.id,
nodeName: node.name,
message: `AI Agent "${node.name}" has 2 language models but needsFallback is not enabled. Set needsFallback=true or remove the second model.`
});
}
}
else if (languageModelConnections.length === 1 && node.parameters.needsFallback === true) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `AI Agent "${node.name}" has needsFallback=true but only 1 language model connected. Connect a second model for fallback or disable needsFallback.`,
code: 'FALLBACK_MISSING_SECOND_MODEL'
});
}
const outputParserConnections = incoming.filter(c => c.type === 'ai_outputParser');
if (node.parameters.hasOutputParser === true) {
if (outputParserConnections.length === 0) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `AI Agent "${node.name}" has hasOutputParser=true but no ai_outputParser connection. Connect an output parser or set hasOutputParser=false.`,
code: 'MISSING_OUTPUT_PARSER'
});
}
}
else if (outputParserConnections.length > 0) {
issues.push({
severity: 'warning',
nodeId: node.id,
nodeName: node.name,
message: `AI Agent "${node.name}" has an output parser connected but hasOutputParser is not true. Set hasOutputParser=true to enable output parsing.`
});
}
if (outputParserConnections.length > 1) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `AI Agent "${node.name}" has ${outputParserConnections.length} output parsers. Only 1 is allowed.`,
code: 'MULTIPLE_OUTPUT_PARSERS'
});
}
if (node.parameters.promptType === 'define') {
if (!node.parameters.text || node.parameters.text.trim() === '') {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `AI Agent "${node.name}" has promptType="define" but the text field is empty. Provide a custom prompt or switch to promptType="auto".`,
code: 'MISSING_PROMPT_TEXT'
});
}
}
if (!node.parameters.systemMessage) {
issues.push({
severity: 'info',
nodeId: node.id,
nodeName: node.name,
message: `AI Agent "${node.name}" has no systemMessage. Consider adding one to define the agent's role, capabilities, and constraints.`
});
}
else if (node.parameters.systemMessage.trim().length < MIN_SYSTEM_MESSAGE_LENGTH) {
issues.push({
severity: 'info',
nodeId: node.id,
nodeName: node.name,
message: `AI Agent "${node.name}" systemMessage is very short (minimum ${MIN_SYSTEM_MESSAGE_LENGTH} characters recommended). Provide more detail about the agent's role and capabilities.`
});
}
const isStreamingTarget = checkIfStreamingTarget(node, workflow, reverseConnections);
const hasOwnStreamingEnabled = node.parameters?.options?.streamResponse === true;
if (isStreamingTarget || hasOwnStreamingEnabled) {
const agentMainOutput = workflow.connections[node.name]?.main;
if (agentMainOutput && agentMainOutput.flat().some((c) => c)) {
const streamSource = isStreamingTarget
? 'connected from Chat Trigger with responseMode="streaming"'
: 'has streamResponse=true in options';
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `AI Agent "${node.name}" is in streaming mode (${streamSource}) but has outgoing main connections. Remove all main output connections - streaming responses flow back through the Chat Trigger.`,
code: 'STREAMING_WITH_MAIN_OUTPUT'
});
}
}
const memoryConnections = incoming.filter(c => c.type === 'ai_memory');
if (memoryConnections.length > 1) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `AI Agent "${node.name}" has ${memoryConnections.length} ai_memory connections. Only 1 memory is allowed.`,
code: 'MULTIPLE_MEMORY_CONNECTIONS'
});
}
const toolConnections = incoming.filter(c => c.type === 'ai_tool');
if (toolConnections.length === 0) {
issues.push({
severity: 'info',
nodeId: node.id,
nodeName: node.name,
message: `AI Agent "${node.name}" has no ai_tool connections. Consider adding tools to enhance the agent's capabilities.`
});
}
if (node.parameters.maxIterations !== undefined) {
if (typeof node.parameters.maxIterations !== 'number') {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `AI Agent "${node.name}" has invalid maxIterations type. Must be a number.`,
code: 'INVALID_MAX_ITERATIONS_TYPE'
});
}
else if (node.parameters.maxIterations < 1) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `AI Agent "${node.name}" has maxIterations=${node.parameters.maxIterations}. Must be at least 1.`,
code: 'MAX_ITERATIONS_TOO_LOW'
});
}
else if (node.parameters.maxIterations > MAX_ITERATIONS_WARNING_THRESHOLD) {
issues.push({
severity: 'warning',
nodeId: node.id,
nodeName: node.name,
message: `AI Agent "${node.name}" has maxIterations=${node.parameters.maxIterations}. Very high iteration counts (>${MAX_ITERATIONS_WARNING_THRESHOLD}) may cause long execution times and high costs.`
});
}
}
return issues;
}
function checkIfStreamingTarget(node, workflow, reverseConnections) {
const incoming = reverseConnections.get(node.name) || [];
const mainConnections = incoming.filter(c => c.type === 'main');
for (const conn of mainConnections) {
const sourceNode = workflow.nodes.find(n => n.name === conn.sourceName);
if (!sourceNode)
continue;
const normalizedType = node_type_normalizer_1.NodeTypeNormalizer.normalizeToFullForm(sourceNode.type);
if (normalizedType === 'nodes-langchain.chatTrigger') {
const responseMode = sourceNode.parameters?.options?.responseMode || 'lastNode';
if (responseMode === 'streaming') {
return true;
}
}
}
return false;
}
function validateChatTrigger(node, workflow, reverseConnections) {
const issues = [];
const responseMode = node.parameters?.options?.responseMode || 'lastNode';
const outgoingMain = workflow.connections[node.name]?.main;
if (!outgoingMain || outgoingMain.length === 0 || !outgoingMain[0] || outgoingMain[0].length === 0) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `Chat Trigger "${node.name}" has no outgoing connections. Connect it to an AI Agent or workflow.`,
code: 'MISSING_CONNECTIONS'
});
return issues;
}
const firstConnection = outgoingMain[0][0];
if (!firstConnection) {
return issues;
}
const targetNode = workflow.nodes.find(n => n.name === firstConnection.node);
if (!targetNode) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `Chat Trigger "${node.name}" connects to non-existent node "${firstConnection.node}".`,
code: 'INVALID_TARGET_NODE'
});
return issues;
}
const targetType = node_type_normalizer_1.NodeTypeNormalizer.normalizeToFullForm(targetNode.type);
if (responseMode === 'streaming') {
if (targetType !== 'nodes-langchain.agent') {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `Chat Trigger "${node.name}" has responseMode="streaming" but connects to "${targetNode.name}" (${targetType}). Streaming mode only works with AI Agent. Change responseMode to "lastNode" or connect to an AI Agent.`,
code: 'STREAMING_WRONG_TARGET'
});
}
else {
const agentMainOutput = workflow.connections[targetNode.name]?.main;
if (agentMainOutput && agentMainOutput.flat().some((c) => c)) {
issues.push({
severity: 'error',
nodeId: targetNode.id,
nodeName: targetNode.name,
message: `AI Agent "${targetNode.name}" is in streaming mode but has outgoing main connections. In streaming mode, the AI Agent must NOT have main output connections - responses stream back through the Chat Trigger.`,
code: 'STREAMING_AGENT_HAS_OUTPUT'
});
}
}
}
if (responseMode === 'lastNode') {
if (targetType === 'nodes-langchain.agent') {
issues.push({
severity: 'info',
nodeId: node.id,
nodeName: node.name,
message: `Chat Trigger "${node.name}" uses responseMode="lastNode" with AI Agent. Consider using responseMode="streaming" for better user experience with real-time responses.`
});
}
}
return issues;
}
function validateBasicLLMChain(node, reverseConnections) {
const issues = [];
const incoming = reverseConnections.get(node.name) || [];
const languageModelConnections = incoming.filter(c => c.type === 'ai_languageModel');
if (languageModelConnections.length === 0) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `Basic LLM Chain "${node.name}" requires an ai_languageModel connection. Connect a language model node.`,
code: 'MISSING_LANGUAGE_MODEL'
});
}
else if (languageModelConnections.length > 1) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `Basic LLM Chain "${node.name}" has ${languageModelConnections.length} ai_languageModel connections. Basic LLM Chain only supports 1 language model (no fallback).`,
code: 'MULTIPLE_LANGUAGE_MODELS'
});
}
const memoryConnections = incoming.filter(c => c.type === 'ai_memory');
if (memoryConnections.length > 1) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `Basic LLM Chain "${node.name}" has ${memoryConnections.length} ai_memory connections. Only 1 memory is allowed.`,
code: 'MULTIPLE_MEMORY_CONNECTIONS'
});
}
const toolConnections = incoming.filter(c => c.type === 'ai_tool');
if (toolConnections.length > 0) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `Basic LLM Chain "${node.name}" has ai_tool connections. Basic LLM Chain does not support tools. Use AI Agent if you need tool support.`,
code: 'TOOLS_NOT_SUPPORTED'
});
}
if (node.parameters.promptType === 'define') {
if (!node.parameters.text || node.parameters.text.trim() === '') {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `Basic LLM Chain "${node.name}" has promptType="define" but the text field is empty.`,
code: 'MISSING_PROMPT_TEXT'
});
}
}
return issues;
}
function validateAISpecificNodes(workflow) {
const issues = [];
const reverseConnectionMap = buildReverseConnectionMap(workflow);
for (const node of workflow.nodes) {
if (node.disabled)
continue;
const normalizedType = node_type_normalizer_1.NodeTypeNormalizer.normalizeToFullForm(node.type);
if (normalizedType === 'nodes-langchain.agent') {
const nodeIssues = validateAIAgent(node, reverseConnectionMap, workflow);
issues.push(...nodeIssues);
}
if (normalizedType === 'nodes-langchain.chatTrigger') {
const nodeIssues = validateChatTrigger(node, workflow, reverseConnectionMap);
issues.push(...nodeIssues);
}
if (normalizedType === 'nodes-langchain.chainLlm') {
const nodeIssues = validateBasicLLMChain(node, reverseConnectionMap);
issues.push(...nodeIssues);
}
if ((0, ai_tool_validators_1.isAIToolSubNode)(normalizedType)) {
const nodeIssues = (0, ai_tool_validators_1.validateAIToolSubNode)(node, normalizedType, reverseConnectionMap, workflow);
issues.push(...nodeIssues);
}
}
return issues;
}
function hasAINodes(workflow) {
const aiNodeTypes = [
'nodes-langchain.agent',
'nodes-langchain.chatTrigger',
'nodes-langchain.chainLlm',
];
return workflow.nodes.some(node => {
const normalized = node_type_normalizer_1.NodeTypeNormalizer.normalizeToFullForm(node.type);
return aiNodeTypes.includes(normalized) || (0, ai_tool_validators_1.isAIToolSubNode)(normalized);
});
}
function getAINodeCategory(nodeType) {
const normalized = node_type_normalizer_1.NodeTypeNormalizer.normalizeToFullForm(nodeType);
if (normalized === 'nodes-langchain.agent')
return 'AI Agent';
if (normalized === 'nodes-langchain.chatTrigger')
return 'Chat Trigger';
if (normalized === 'nodes-langchain.chainLlm')
return 'Basic LLM Chain';
if ((0, ai_tool_validators_1.isAIToolSubNode)(normalized))
return 'AI Tool';
if (normalized.startsWith('nodes-langchain.')) {
if (normalized.includes('openAi') || normalized.includes('anthropic') || normalized.includes('googleGemini')) {
return 'Language Model';
}
if (normalized.includes('memory') || normalized.includes('buffer')) {
return 'Memory';
}
if (normalized.includes('vectorStore') || normalized.includes('pinecone') || normalized.includes('qdrant')) {
return 'Vector Store';
}
if (normalized.includes('embedding')) {
return 'Embeddings';
}
return 'AI Component';
}
return null;
}
//# sourceMappingURL=ai-node-validator.js.map

File diff suppressed because one or more lines are too long

58
dist/services/ai-tool-validators.d.ts vendored Normal file
View File

@@ -0,0 +1,58 @@
export interface WorkflowNode {
id: string;
name: string;
type: string;
position: [number, number];
parameters: any;
credentials?: any;
disabled?: boolean;
typeVersion?: number;
}
export interface WorkflowJson {
name?: string;
nodes: WorkflowNode[];
connections: Record<string, any>;
settings?: any;
}
export interface ReverseConnection {
sourceName: string;
sourceType: string;
type: string;
index: number;
}
export interface ValidationIssue {
severity: 'error' | 'warning' | 'info';
nodeId?: string;
nodeName?: string;
message: string;
code?: string;
}
export declare function validateHTTPRequestTool(node: WorkflowNode): ValidationIssue[];
export declare function validateCodeTool(node: WorkflowNode): ValidationIssue[];
export declare function validateVectorStoreTool(node: WorkflowNode, reverseConnections: Map<string, ReverseConnection[]>, workflow: WorkflowJson): ValidationIssue[];
export declare function validateWorkflowTool(node: WorkflowNode, reverseConnections?: Map<string, ReverseConnection[]>): ValidationIssue[];
export declare function validateAIAgentTool(node: WorkflowNode, reverseConnections: Map<string, ReverseConnection[]>): ValidationIssue[];
export declare function validateMCPClientTool(node: WorkflowNode): ValidationIssue[];
export declare function validateCalculatorTool(node: WorkflowNode): ValidationIssue[];
export declare function validateThinkTool(node: WorkflowNode): ValidationIssue[];
export declare function validateSerpApiTool(node: WorkflowNode): ValidationIssue[];
export declare function validateWikipediaTool(node: WorkflowNode): ValidationIssue[];
export declare function validateSearXngTool(node: WorkflowNode): ValidationIssue[];
export declare function validateWolframAlphaTool(node: WorkflowNode): ValidationIssue[];
export declare const AI_TOOL_VALIDATORS: {
readonly 'nodes-langchain.toolHttpRequest': typeof validateHTTPRequestTool;
readonly 'nodes-langchain.toolCode': typeof validateCodeTool;
readonly 'nodes-langchain.toolVectorStore': typeof validateVectorStoreTool;
readonly 'nodes-langchain.toolWorkflow': typeof validateWorkflowTool;
readonly 'nodes-langchain.agentTool': typeof validateAIAgentTool;
readonly 'nodes-langchain.mcpClientTool': typeof validateMCPClientTool;
readonly 'nodes-langchain.toolCalculator': typeof validateCalculatorTool;
readonly 'nodes-langchain.toolThink': typeof validateThinkTool;
readonly 'nodes-langchain.toolSerpApi': typeof validateSerpApiTool;
readonly 'nodes-langchain.toolWikipedia': typeof validateWikipediaTool;
readonly 'nodes-langchain.toolSearXng': typeof validateSearXngTool;
readonly 'nodes-langchain.toolWolframAlpha': typeof validateWolframAlphaTool;
};
export declare function isAIToolSubNode(nodeType: string): boolean;
export declare function validateAIToolSubNode(node: WorkflowNode, nodeType: string, reverseConnections: Map<string, ReverseConnection[]>, workflow: WorkflowJson): ValidationIssue[];
//# sourceMappingURL=ai-tool-validators.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ai-tool-validators.d.ts","sourceRoot":"","sources":["../../src/services/ai-tool-validators.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3B,UAAU,EAAE,GAAG,CAAC;IAChB,WAAW,CAAC,EAAE,GAAG,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACjC,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAMD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,EAAE,CAuJ7E;AAMD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,EAAE,CAoCtE;AAMD,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,YAAY,EAClB,kBAAkB,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,EAAE,CAAC,EACpD,QAAQ,EAAE,YAAY,GACrB,eAAe,EAAE,CAmCnB;AAMD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,YAAY,EAAE,kBAAkB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,EAAE,CAAC,GAAG,eAAe,EAAE,CA0BjI;AAMD,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,YAAY,EAClB,kBAAkB,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,EAAE,CAAC,GACnD,eAAe,EAAE,CAmCnB;AAMD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,EAAE,CA0B3E;AAMD,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,EAAE,CAM5E;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,EAAE,CAMvE;AAMD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,EAAE,CAyBzE;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,EAAE,CA4B3E;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,EAAE,CA0BzE;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,EAAE,CAyB9E;AAKD,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;CAarB,CAAC;AAKX,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGzD;AAKD,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,YAAY,EAClB,QAAQ,EAAE,MAAM,EAChB,kBAAkB,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,EAAE,CAAC,EACpD,QAAQ,EAAE,YAAY,GACrB,eAAe,EAAE,CAgCnB"}

438
dist/services/ai-tool-validators.js vendored Normal file
View File

@@ -0,0 +1,438 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AI_TOOL_VALIDATORS = void 0;
exports.validateHTTPRequestTool = validateHTTPRequestTool;
exports.validateCodeTool = validateCodeTool;
exports.validateVectorStoreTool = validateVectorStoreTool;
exports.validateWorkflowTool = validateWorkflowTool;
exports.validateAIAgentTool = validateAIAgentTool;
exports.validateMCPClientTool = validateMCPClientTool;
exports.validateCalculatorTool = validateCalculatorTool;
exports.validateThinkTool = validateThinkTool;
exports.validateSerpApiTool = validateSerpApiTool;
exports.validateWikipediaTool = validateWikipediaTool;
exports.validateSearXngTool = validateSearXngTool;
exports.validateWolframAlphaTool = validateWolframAlphaTool;
exports.isAIToolSubNode = isAIToolSubNode;
exports.validateAIToolSubNode = validateAIToolSubNode;
const node_type_normalizer_1 = require("../utils/node-type-normalizer");
const MIN_DESCRIPTION_LENGTH_SHORT = 10;
const MIN_DESCRIPTION_LENGTH_MEDIUM = 15;
const MIN_DESCRIPTION_LENGTH_LONG = 20;
const MAX_ITERATIONS_WARNING_THRESHOLD = 50;
const MAX_TOPK_WARNING_THRESHOLD = 20;
function validateHTTPRequestTool(node) {
const issues = [];
if (!node.parameters.toolDescription) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `HTTP Request Tool "${node.name}" has no toolDescription. Add a clear description to help the LLM know when to use this API.`,
code: 'MISSING_TOOL_DESCRIPTION'
});
}
else if (node.parameters.toolDescription.trim().length < MIN_DESCRIPTION_LENGTH_MEDIUM) {
issues.push({
severity: 'warning',
nodeId: node.id,
nodeName: node.name,
message: `HTTP Request Tool "${node.name}" toolDescription is too short (minimum ${MIN_DESCRIPTION_LENGTH_MEDIUM} characters). Explain what API this calls and when to use it.`
});
}
if (!node.parameters.url) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `HTTP Request Tool "${node.name}" has no URL. Add the API endpoint URL.`,
code: 'MISSING_URL'
});
}
else {
try {
const urlObj = new URL(node.parameters.url);
if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `HTTP Request Tool "${node.name}" has invalid URL protocol "${urlObj.protocol}". Use http:// or https:// only.`,
code: 'INVALID_URL_PROTOCOL'
});
}
}
catch (e) {
if (!node.parameters.url.includes('{{')) {
issues.push({
severity: 'warning',
nodeId: node.id,
nodeName: node.name,
message: `HTTP Request Tool "${node.name}" has potentially invalid URL format. Ensure it's a valid URL or n8n expression.`
});
}
}
}
if (node.parameters.url || node.parameters.body || node.parameters.headers) {
const placeholderRegex = /\{([^}]+)\}/g;
const placeholders = new Set();
[node.parameters.url, node.parameters.body, JSON.stringify(node.parameters.headers || {})].forEach(text => {
if (text) {
let match;
while ((match = placeholderRegex.exec(text)) !== null) {
placeholders.add(match[1]);
}
}
});
if (placeholders.size > 0) {
const definitions = node.parameters.placeholderDefinitions?.values || [];
const definedNames = new Set(definitions.map((d) => d.name));
if (!node.parameters.placeholderDefinitions) {
issues.push({
severity: 'warning',
nodeId: node.id,
nodeName: node.name,
message: `HTTP Request Tool "${node.name}" uses placeholders but has no placeholderDefinitions. Add definitions to describe the expected inputs.`
});
}
else {
for (const placeholder of placeholders) {
if (!definedNames.has(placeholder)) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `HTTP Request Tool "${node.name}" Placeholder "${placeholder}" in URL but it's not defined in placeholderDefinitions.`,
code: 'UNDEFINED_PLACEHOLDER'
});
}
}
for (const def of definitions) {
if (!placeholders.has(def.name)) {
issues.push({
severity: 'warning',
nodeId: node.id,
nodeName: node.name,
message: `HTTP Request Tool "${node.name}" defines placeholder "${def.name}" but doesn't use it.`
});
}
}
}
}
}
if (node.parameters.authentication === 'predefinedCredentialType' &&
(!node.credentials || Object.keys(node.credentials).length === 0)) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `HTTP Request Tool "${node.name}" requires credentials but none are configured.`,
code: 'MISSING_CREDENTIALS'
});
}
const validMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
if (node.parameters.method && !validMethods.includes(node.parameters.method.toUpperCase())) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `HTTP Request Tool "${node.name}" has invalid HTTP method "${node.parameters.method}". Use one of: ${validMethods.join(', ')}.`,
code: 'INVALID_HTTP_METHOD'
});
}
if (['POST', 'PUT', 'PATCH'].includes(node.parameters.method?.toUpperCase())) {
if (!node.parameters.body && !node.parameters.jsonBody) {
issues.push({
severity: 'warning',
nodeId: node.id,
nodeName: node.name,
message: `HTTP Request Tool "${node.name}" uses ${node.parameters.method} but has no body. Consider adding a body or using GET instead.`
});
}
}
return issues;
}
function validateCodeTool(node) {
const issues = [];
if (!node.parameters.toolDescription) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `Code Tool "${node.name}" has no toolDescription. Add one to help the LLM understand the tool's purpose.`,
code: 'MISSING_TOOL_DESCRIPTION'
});
}
if (!node.parameters.jsCode || node.parameters.jsCode.trim().length === 0) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `Code Tool "${node.name}" code is empty. Add the JavaScript code to execute.`,
code: 'MISSING_CODE'
});
}
if (!node.parameters.inputSchema && !node.parameters.specifyInputSchema) {
issues.push({
severity: 'warning',
nodeId: node.id,
nodeName: node.name,
message: `Code Tool "${node.name}" has no input schema. Consider adding one to validate LLM inputs.`
});
}
return issues;
}
function validateVectorStoreTool(node, reverseConnections, workflow) {
const issues = [];
if (!node.parameters.toolDescription) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `Vector Store Tool "${node.name}" has no toolDescription. Add one to explain what data it searches.`,
code: 'MISSING_TOOL_DESCRIPTION'
});
}
if (node.parameters.topK !== undefined) {
if (typeof node.parameters.topK !== 'number' || node.parameters.topK < 1) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `Vector Store Tool "${node.name}" has invalid topK value. Must be a positive number.`,
code: 'INVALID_TOPK'
});
}
else if (node.parameters.topK > MAX_TOPK_WARNING_THRESHOLD) {
issues.push({
severity: 'warning',
nodeId: node.id,
nodeName: node.name,
message: `Vector Store Tool "${node.name}" has topK=${node.parameters.topK}. Large values (>${MAX_TOPK_WARNING_THRESHOLD}) may overwhelm the LLM context. Consider reducing to 10 or less.`
});
}
}
return issues;
}
function validateWorkflowTool(node, reverseConnections) {
const issues = [];
if (!node.parameters.toolDescription) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `Workflow Tool "${node.name}" has no toolDescription. Add one to help the LLM know when to use this tool.`,
code: 'MISSING_TOOL_DESCRIPTION'
});
}
if (!node.parameters.workflowId) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `Workflow Tool "${node.name}" has no workflowId. Select a workflow to execute.`,
code: 'MISSING_WORKFLOW_ID'
});
}
return issues;
}
function validateAIAgentTool(node, reverseConnections) {
const issues = [];
if (!node.parameters.toolDescription) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `AI Agent Tool "${node.name}" has no toolDescription. Add one to help the LLM know when to use this tool.`,
code: 'MISSING_TOOL_DESCRIPTION'
});
}
if (node.parameters.maxIterations !== undefined) {
if (typeof node.parameters.maxIterations !== 'number' || node.parameters.maxIterations < 1) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `AI Agent Tool "${node.name}" has invalid maxIterations. Must be a positive number.`,
code: 'INVALID_MAX_ITERATIONS'
});
}
else if (node.parameters.maxIterations > MAX_ITERATIONS_WARNING_THRESHOLD) {
issues.push({
severity: 'warning',
nodeId: node.id,
nodeName: node.name,
message: `AI Agent Tool "${node.name}" has maxIterations=${node.parameters.maxIterations}. Large values (>${MAX_ITERATIONS_WARNING_THRESHOLD}) may lead to long execution times.`
});
}
}
return issues;
}
function validateMCPClientTool(node) {
const issues = [];
if (!node.parameters.toolDescription) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `MCP Client Tool "${node.name}" has no toolDescription. Add one to help the LLM know when to use this tool.`,
code: 'MISSING_TOOL_DESCRIPTION'
});
}
if (!node.parameters.serverUrl) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `MCP Client Tool "${node.name}" has no serverUrl. Configure the MCP server URL.`,
code: 'MISSING_SERVER_URL'
});
}
return issues;
}
function validateCalculatorTool(node) {
const issues = [];
return issues;
}
function validateThinkTool(node) {
const issues = [];
return issues;
}
function validateSerpApiTool(node) {
const issues = [];
if (!node.parameters.toolDescription) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `SerpApi Tool "${node.name}" has no toolDescription. Add one to explain when to use Google search.`,
code: 'MISSING_TOOL_DESCRIPTION'
});
}
if (!node.credentials || !node.credentials.serpApiApi) {
issues.push({
severity: 'warning',
nodeId: node.id,
nodeName: node.name,
message: `SerpApi Tool "${node.name}" requires SerpApi credentials. Configure your API key.`
});
}
return issues;
}
function validateWikipediaTool(node) {
const issues = [];
if (!node.parameters.toolDescription) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `Wikipedia Tool "${node.name}" has no toolDescription. Add one to explain when to use Wikipedia.`,
code: 'MISSING_TOOL_DESCRIPTION'
});
}
if (node.parameters.language) {
const validLanguageCodes = /^[a-z]{2,3}$/;
if (!validLanguageCodes.test(node.parameters.language)) {
issues.push({
severity: 'warning',
nodeId: node.id,
nodeName: node.name,
message: `Wikipedia Tool "${node.name}" has potentially invalid language code "${node.parameters.language}". Use ISO 639 codes (e.g., "en", "es", "fr").`
});
}
}
return issues;
}
function validateSearXngTool(node) {
const issues = [];
if (!node.parameters.toolDescription) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `SearXNG Tool "${node.name}" has no toolDescription. Add one to explain when to use SearXNG.`,
code: 'MISSING_TOOL_DESCRIPTION'
});
}
if (!node.parameters.baseUrl) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `SearXNG Tool "${node.name}" has no baseUrl. Configure your SearXNG instance URL.`,
code: 'MISSING_BASE_URL'
});
}
return issues;
}
function validateWolframAlphaTool(node) {
const issues = [];
if (!node.credentials || (!node.credentials.wolframAlpha && !node.credentials.wolframAlphaApi)) {
issues.push({
severity: 'error',
nodeId: node.id,
nodeName: node.name,
message: `WolframAlpha Tool "${node.name}" requires Wolfram|Alpha API credentials. Configure your App ID.`,
code: 'MISSING_CREDENTIALS'
});
}
if (!node.parameters.description && !node.parameters.toolDescription) {
issues.push({
severity: 'info',
nodeId: node.id,
nodeName: node.name,
message: `WolframAlpha Tool "${node.name}" has no custom description. Add one to explain when to use Wolfram|Alpha for computational queries.`
});
}
return issues;
}
exports.AI_TOOL_VALIDATORS = {
'nodes-langchain.toolHttpRequest': validateHTTPRequestTool,
'nodes-langchain.toolCode': validateCodeTool,
'nodes-langchain.toolVectorStore': validateVectorStoreTool,
'nodes-langchain.toolWorkflow': validateWorkflowTool,
'nodes-langchain.agentTool': validateAIAgentTool,
'nodes-langchain.mcpClientTool': validateMCPClientTool,
'nodes-langchain.toolCalculator': validateCalculatorTool,
'nodes-langchain.toolThink': validateThinkTool,
'nodes-langchain.toolSerpApi': validateSerpApiTool,
'nodes-langchain.toolWikipedia': validateWikipediaTool,
'nodes-langchain.toolSearXng': validateSearXngTool,
'nodes-langchain.toolWolframAlpha': validateWolframAlphaTool,
};
function isAIToolSubNode(nodeType) {
const normalized = node_type_normalizer_1.NodeTypeNormalizer.normalizeToFullForm(nodeType);
return normalized in exports.AI_TOOL_VALIDATORS;
}
function validateAIToolSubNode(node, nodeType, reverseConnections, workflow) {
const normalized = node_type_normalizer_1.NodeTypeNormalizer.normalizeToFullForm(nodeType);
switch (normalized) {
case 'nodes-langchain.toolHttpRequest':
return validateHTTPRequestTool(node);
case 'nodes-langchain.toolCode':
return validateCodeTool(node);
case 'nodes-langchain.toolVectorStore':
return validateVectorStoreTool(node, reverseConnections, workflow);
case 'nodes-langchain.toolWorkflow':
return validateWorkflowTool(node);
case 'nodes-langchain.agentTool':
return validateAIAgentTool(node, reverseConnections);
case 'nodes-langchain.mcpClientTool':
return validateMCPClientTool(node);
case 'nodes-langchain.toolCalculator':
return validateCalculatorTool(node);
case 'nodes-langchain.toolThink':
return validateThinkTool(node);
case 'nodes-langchain.toolSerpApi':
return validateSerpApiTool(node);
case 'nodes-langchain.toolWikipedia':
return validateWikipediaTool(node);
case 'nodes-langchain.toolSearXng':
return validateSearXngTool(node);
case 'nodes-langchain.toolWolframAlpha':
return validateWolframAlphaTool(node);
default:
return [];
}
}
//# sourceMappingURL=ai-tool-validators.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,38 @@
import { NodeRepository } from '../database/node-repository';
export interface DetectedChange {
propertyName: string;
changeType: 'added' | 'removed' | 'renamed' | 'type_changed' | 'requirement_changed' | 'default_changed';
isBreaking: boolean;
oldValue?: any;
newValue?: any;
migrationHint: string;
autoMigratable: boolean;
migrationStrategy?: any;
severity: 'LOW' | 'MEDIUM' | 'HIGH';
source: 'registry' | 'dynamic';
}
export interface VersionUpgradeAnalysis {
nodeType: string;
fromVersion: string;
toVersion: string;
hasBreakingChanges: boolean;
changes: DetectedChange[];
autoMigratableCount: number;
manualRequiredCount: number;
overallSeverity: 'LOW' | 'MEDIUM' | 'HIGH';
recommendations: string[];
}
export declare class BreakingChangeDetector {
private nodeRepository;
constructor(nodeRepository: NodeRepository);
analyzeVersionUpgrade(nodeType: string, fromVersion: string, toVersion: string): Promise<VersionUpgradeAnalysis>;
private getRegistryChanges;
private detectDynamicChanges;
private flattenProperties;
private mergeChanges;
private calculateOverallSeverity;
private generateRecommendations;
hasBreakingChanges(nodeType: string, fromVersion: string, toVersion: string): boolean;
getChangedProperties(nodeType: string, fromVersion: string, toVersion: string): string[];
}
//# sourceMappingURL=breaking-change-detector.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"breaking-change-detector.d.ts","sourceRoot":"","sources":["../../src/services/breaking-change-detector.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAQ7D,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,cAAc,GAAG,qBAAqB,GAAG,iBAAiB,CAAC;IACzG,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,GAAG,CAAC;IACf,QAAQ,CAAC,EAAE,GAAG,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,OAAO,CAAC;IACxB,iBAAiB,CAAC,EAAE,GAAG,CAAC;IACxB,QAAQ,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACpC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAC;CAChC;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,eAAe,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC3C,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,qBAAa,sBAAsB;IACrB,OAAO,CAAC,cAAc;gBAAd,cAAc,EAAE,cAAc;IAK5C,qBAAqB,CACzB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,sBAAsB,CAAC;IAqClC,OAAO,CAAC,kBAAkB;IAwB1B,OAAO,CAAC,oBAAoB;IA+F5B,OAAO,CAAC,iBAAiB;IAuBzB,OAAO,CAAC,YAAY;IA4BpB,OAAO,CAAC,wBAAwB;IAShC,OAAO,CAAC,uBAAuB;IAsC/B,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO;IAQrF,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE;CAIzF"}

View File

@@ -0,0 +1,184 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BreakingChangeDetector = void 0;
const breaking_changes_registry_1 = require("./breaking-changes-registry");
class BreakingChangeDetector {
constructor(nodeRepository) {
this.nodeRepository = nodeRepository;
}
async analyzeVersionUpgrade(nodeType, fromVersion, toVersion) {
const registryChanges = this.getRegistryChanges(nodeType, fromVersion, toVersion);
const dynamicChanges = this.detectDynamicChanges(nodeType, fromVersion, toVersion);
const allChanges = this.mergeChanges(registryChanges, dynamicChanges);
const hasBreakingChanges = allChanges.some(c => c.isBreaking);
const autoMigratableCount = allChanges.filter(c => c.autoMigratable).length;
const manualRequiredCount = allChanges.filter(c => !c.autoMigratable).length;
const overallSeverity = this.calculateOverallSeverity(allChanges);
const recommendations = this.generateRecommendations(allChanges);
return {
nodeType,
fromVersion,
toVersion,
hasBreakingChanges,
changes: allChanges,
autoMigratableCount,
manualRequiredCount,
overallSeverity,
recommendations
};
}
getRegistryChanges(nodeType, fromVersion, toVersion) {
const registryChanges = (0, breaking_changes_registry_1.getAllChangesForNode)(nodeType, fromVersion, toVersion);
return registryChanges.map(change => ({
propertyName: change.propertyName,
changeType: change.changeType,
isBreaking: change.isBreaking,
oldValue: change.oldValue,
newValue: change.newValue,
migrationHint: change.migrationHint,
autoMigratable: change.autoMigratable,
migrationStrategy: change.migrationStrategy,
severity: change.severity,
source: 'registry'
}));
}
detectDynamicChanges(nodeType, fromVersion, toVersion) {
const oldVersionData = this.nodeRepository.getNodeVersion(nodeType, fromVersion);
const newVersionData = this.nodeRepository.getNodeVersion(nodeType, toVersion);
if (!oldVersionData || !newVersionData) {
return [];
}
const changes = [];
const oldProps = this.flattenProperties(oldVersionData.propertiesSchema || []);
const newProps = this.flattenProperties(newVersionData.propertiesSchema || []);
for (const propName of Object.keys(newProps)) {
if (!oldProps[propName]) {
const prop = newProps[propName];
const isRequired = prop.required === true;
changes.push({
propertyName: propName,
changeType: 'added',
isBreaking: isRequired,
newValue: prop.type || 'unknown',
migrationHint: isRequired
? `Property "${propName}" is now required in v${toVersion}. Provide a value to prevent validation errors.`
: `Property "${propName}" was added in v${toVersion}. Optional parameter, safe to ignore if not needed.`,
autoMigratable: !isRequired,
migrationStrategy: !isRequired
? {
type: 'add_property',
defaultValue: prop.default || null
}
: undefined,
severity: isRequired ? 'HIGH' : 'LOW',
source: 'dynamic'
});
}
}
for (const propName of Object.keys(oldProps)) {
if (!newProps[propName]) {
changes.push({
propertyName: propName,
changeType: 'removed',
isBreaking: true,
oldValue: oldProps[propName].type || 'unknown',
migrationHint: `Property "${propName}" was removed in v${toVersion}. Remove this property from your configuration.`,
autoMigratable: true,
migrationStrategy: {
type: 'remove_property'
},
severity: 'MEDIUM',
source: 'dynamic'
});
}
}
for (const propName of Object.keys(newProps)) {
if (oldProps[propName]) {
const oldRequired = oldProps[propName].required === true;
const newRequired = newProps[propName].required === true;
if (oldRequired !== newRequired) {
changes.push({
propertyName: propName,
changeType: 'requirement_changed',
isBreaking: newRequired && !oldRequired,
oldValue: oldRequired ? 'required' : 'optional',
newValue: newRequired ? 'required' : 'optional',
migrationHint: newRequired
? `Property "${propName}" is now required in v${toVersion}. Ensure a value is provided.`
: `Property "${propName}" is now optional in v${toVersion}.`,
autoMigratable: false,
severity: newRequired ? 'HIGH' : 'LOW',
source: 'dynamic'
});
}
}
}
return changes;
}
flattenProperties(properties, prefix = '') {
const flat = {};
for (const prop of properties) {
if (!prop.name && !prop.displayName)
continue;
const propName = prop.name || prop.displayName;
const fullPath = prefix ? `${prefix}.${propName}` : propName;
flat[fullPath] = prop;
if (prop.options && Array.isArray(prop.options)) {
Object.assign(flat, this.flattenProperties(prop.options, fullPath));
}
}
return flat;
}
mergeChanges(registryChanges, dynamicChanges) {
const merged = [...registryChanges];
for (const dynamicChange of dynamicChanges) {
const existsInRegistry = registryChanges.some(rc => rc.propertyName === dynamicChange.propertyName &&
rc.changeType === dynamicChange.changeType);
if (!existsInRegistry) {
merged.push(dynamicChange);
}
}
const severityOrder = { HIGH: 0, MEDIUM: 1, LOW: 2 };
merged.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
return merged;
}
calculateOverallSeverity(changes) {
if (changes.some(c => c.severity === 'HIGH'))
return 'HIGH';
if (changes.some(c => c.severity === 'MEDIUM'))
return 'MEDIUM';
return 'LOW';
}
generateRecommendations(changes) {
const recommendations = [];
const breakingChanges = changes.filter(c => c.isBreaking);
const autoMigratable = changes.filter(c => c.autoMigratable);
const manualRequired = changes.filter(c => !c.autoMigratable);
if (breakingChanges.length === 0) {
recommendations.push('✓ No breaking changes detected. This upgrade should be safe.');
}
else {
recommendations.push(`${breakingChanges.length} breaking change(s) detected. Review carefully before applying.`);
}
if (autoMigratable.length > 0) {
recommendations.push(`${autoMigratable.length} change(s) can be automatically migrated.`);
}
if (manualRequired.length > 0) {
recommendations.push(`${manualRequired.length} change(s) require manual intervention.`);
for (const change of manualRequired) {
recommendations.push(` - ${change.propertyName}: ${change.migrationHint}`);
}
}
return recommendations;
}
hasBreakingChanges(nodeType, fromVersion, toVersion) {
const registryChanges = (0, breaking_changes_registry_1.getBreakingChangesForNode)(nodeType, fromVersion, toVersion);
return registryChanges.length > 0;
}
getChangedProperties(nodeType, fromVersion, toVersion) {
const registryChanges = (0, breaking_changes_registry_1.getAllChangesForNode)(nodeType, fromVersion, toVersion);
return registryChanges.map(c => c.propertyName);
}
}
exports.BreakingChangeDetector = BreakingChangeDetector;
//# sourceMappingURL=breaking-change-detector.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,28 @@
export interface BreakingChange {
nodeType: string;
fromVersion: string;
toVersion: string;
propertyName: string;
changeType: 'added' | 'removed' | 'renamed' | 'type_changed' | 'requirement_changed' | 'default_changed';
isBreaking: boolean;
oldValue?: string;
newValue?: string;
migrationHint: string;
autoMigratable: boolean;
migrationStrategy?: {
type: 'add_property' | 'remove_property' | 'rename_property' | 'set_default';
defaultValue?: any;
sourceProperty?: string;
targetProperty?: string;
};
severity: 'LOW' | 'MEDIUM' | 'HIGH';
}
export declare const BREAKING_CHANGES_REGISTRY: BreakingChange[];
export declare function getBreakingChangesForNode(nodeType: string, fromVersion: string, toVersion: string): BreakingChange[];
export declare function getAllChangesForNode(nodeType: string, fromVersion: string, toVersion: string): BreakingChange[];
export declare function getAutoMigratableChanges(nodeType: string, fromVersion: string, toVersion: string): BreakingChange[];
export declare function hasBreakingChanges(nodeType: string, fromVersion: string, toVersion: string): boolean;
export declare function getMigrationHints(nodeType: string, fromVersion: string, toVersion: string): string[];
export declare function getNodesWithVersionMigrations(): string[];
export declare function getTrackedVersionsForNode(nodeType: string): string[];
//# sourceMappingURL=breaking-changes-registry.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"breaking-changes-registry.d.ts","sourceRoot":"","sources":["../../src/services/breaking-changes-registry.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,cAAc,GAAG,qBAAqB,GAAG,iBAAiB,CAAC;IACzG,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,OAAO,CAAC;IACxB,iBAAiB,CAAC,EAAE;QAClB,IAAI,EAAE,cAAc,GAAG,iBAAiB,GAAG,iBAAiB,GAAG,aAAa,CAAC;QAC7E,YAAY,CAAC,EAAE,GAAG,CAAC;QACnB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;IACF,QAAQ,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;CACrC;AAKD,eAAO,MAAM,yBAAyB,EAAE,cAAc,EAyJrD,CAAC;AAKF,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB,cAAc,EAAE,CAYlB;AAKD,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB,cAAc,EAAE,CASlB;AAKD,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB,cAAc,EAAE,CAIlB;AAKD,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB,OAAO,CAET;AAKD,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB,MAAM,EAAE,CAGV;AAwBD,wBAAgB,6BAA6B,IAAI,MAAM,EAAE,CAUxD;AAKD,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAWpE"}

View File

@@ -0,0 +1,200 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BREAKING_CHANGES_REGISTRY = void 0;
exports.getBreakingChangesForNode = getBreakingChangesForNode;
exports.getAllChangesForNode = getAllChangesForNode;
exports.getAutoMigratableChanges = getAutoMigratableChanges;
exports.hasBreakingChanges = hasBreakingChanges;
exports.getMigrationHints = getMigrationHints;
exports.getNodesWithVersionMigrations = getNodesWithVersionMigrations;
exports.getTrackedVersionsForNode = getTrackedVersionsForNode;
exports.BREAKING_CHANGES_REGISTRY = [
{
nodeType: 'n8n-nodes-base.executeWorkflow',
fromVersion: '1.0',
toVersion: '1.1',
propertyName: 'parameters.inputFieldMapping',
changeType: 'added',
isBreaking: true,
migrationHint: 'In v1.1+, the Execute Workflow node requires explicit field mapping to pass data to sub-workflows. Add an "inputFieldMapping" object with "mappings" array defining how to map fields from parent to child workflow.',
autoMigratable: true,
migrationStrategy: {
type: 'add_property',
defaultValue: {
mappings: []
}
},
severity: 'HIGH'
},
{
nodeType: 'n8n-nodes-base.executeWorkflow',
fromVersion: '1.0',
toVersion: '1.1',
propertyName: 'parameters.mode',
changeType: 'requirement_changed',
isBreaking: false,
migrationHint: 'The "mode" parameter behavior changed in v1.1. Default is now "static" instead of "list". Ensure your workflow ID specification matches the selected mode.',
autoMigratable: false,
severity: 'MEDIUM'
},
{
nodeType: 'n8n-nodes-base.webhook',
fromVersion: '2.0',
toVersion: '2.1',
propertyName: 'webhookId',
changeType: 'added',
isBreaking: true,
migrationHint: 'In v2.1+, webhooks require a unique "webhookId" field in addition to the path. This ensures webhook persistence across workflow updates. A UUID will be auto-generated if not provided.',
autoMigratable: true,
migrationStrategy: {
type: 'add_property',
defaultValue: null
},
severity: 'HIGH'
},
{
nodeType: 'n8n-nodes-base.webhook',
fromVersion: '1.0',
toVersion: '2.0',
propertyName: 'parameters.path',
changeType: 'requirement_changed',
isBreaking: true,
migrationHint: 'In v2.0+, the webhook path must be explicitly defined and cannot be empty. Ensure a valid path is set.',
autoMigratable: false,
severity: 'HIGH'
},
{
nodeType: 'n8n-nodes-base.webhook',
fromVersion: '1.0',
toVersion: '2.0',
propertyName: 'parameters.responseMode',
changeType: 'added',
isBreaking: false,
migrationHint: 'v2.0 introduces a "responseMode" parameter to control how the webhook responds. Default is "onReceived" (immediate response). Use "lastNode" to wait for workflow completion.',
autoMigratable: true,
migrationStrategy: {
type: 'add_property',
defaultValue: 'onReceived'
},
severity: 'LOW'
},
{
nodeType: 'n8n-nodes-base.httpRequest',
fromVersion: '4.1',
toVersion: '4.2',
propertyName: 'parameters.sendBody',
changeType: 'requirement_changed',
isBreaking: false,
migrationHint: 'In v4.2+, "sendBody" must be explicitly set to true for POST/PUT/PATCH requests to include a body. Previous versions had implicit body sending.',
autoMigratable: true,
migrationStrategy: {
type: 'add_property',
defaultValue: true
},
severity: 'MEDIUM'
},
{
nodeType: 'n8n-nodes-base.code',
fromVersion: '1.0',
toVersion: '2.0',
propertyName: 'parameters.mode',
changeType: 'added',
isBreaking: false,
migrationHint: 'v2.0 introduces execution modes: "runOnceForAllItems" (default) and "runOnceForEachItem". The default mode processes all items at once, which may differ from v1.0 behavior.',
autoMigratable: true,
migrationStrategy: {
type: 'add_property',
defaultValue: 'runOnceForAllItems'
},
severity: 'MEDIUM'
},
{
nodeType: 'n8n-nodes-base.scheduleTrigger',
fromVersion: '1.0',
toVersion: '1.1',
propertyName: 'parameters.rule.interval',
changeType: 'type_changed',
isBreaking: true,
oldValue: 'string',
newValue: 'array',
migrationHint: 'In v1.1+, the interval parameter changed from a single string to an array of interval objects. Convert your single interval to an array format: [{field: "hours", value: 1}]',
autoMigratable: false,
severity: 'HIGH'
},
{
nodeType: '*',
fromVersion: '1.0',
toVersion: '2.0',
propertyName: 'continueOnFail',
changeType: 'removed',
isBreaking: false,
migrationHint: 'The "continueOnFail" property is deprecated. Use "onError" instead with value "continueErrorOutput" or "continueRegularOutput".',
autoMigratable: true,
migrationStrategy: {
type: 'rename_property',
sourceProperty: 'continueOnFail',
targetProperty: 'onError',
defaultValue: 'continueErrorOutput'
},
severity: 'MEDIUM'
}
];
function getBreakingChangesForNode(nodeType, fromVersion, toVersion) {
return exports.BREAKING_CHANGES_REGISTRY.filter(change => {
const nodeMatches = change.nodeType === nodeType || change.nodeType === '*';
const versionMatches = compareVersions(fromVersion, change.fromVersion) >= 0 &&
compareVersions(toVersion, change.toVersion) <= 0;
return nodeMatches && versionMatches && change.isBreaking;
});
}
function getAllChangesForNode(nodeType, fromVersion, toVersion) {
return exports.BREAKING_CHANGES_REGISTRY.filter(change => {
const nodeMatches = change.nodeType === nodeType || change.nodeType === '*';
const versionMatches = compareVersions(fromVersion, change.fromVersion) >= 0 &&
compareVersions(toVersion, change.toVersion) <= 0;
return nodeMatches && versionMatches;
});
}
function getAutoMigratableChanges(nodeType, fromVersion, toVersion) {
return getAllChangesForNode(nodeType, fromVersion, toVersion).filter(change => change.autoMigratable);
}
function hasBreakingChanges(nodeType, fromVersion, toVersion) {
return getBreakingChangesForNode(nodeType, fromVersion, toVersion).length > 0;
}
function getMigrationHints(nodeType, fromVersion, toVersion) {
const changes = getAllChangesForNode(nodeType, fromVersion, toVersion);
return changes.map(change => change.migrationHint);
}
function compareVersions(v1, v2) {
const parts1 = v1.split('.').map(Number);
const parts2 = v2.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;
}
function getNodesWithVersionMigrations() {
const nodeTypes = new Set();
exports.BREAKING_CHANGES_REGISTRY.forEach(change => {
if (change.nodeType !== '*') {
nodeTypes.add(change.nodeType);
}
});
return Array.from(nodeTypes);
}
function getTrackedVersionsForNode(nodeType) {
const versions = new Set();
exports.BREAKING_CHANGES_REGISTRY
.filter(change => change.nodeType === nodeType || change.nodeType === '*')
.forEach(change => {
versions.add(change.fromVersion);
versions.add(change.toVersion);
});
return Array.from(versions).sort((a, b) => compareVersions(a, b));
}
//# sourceMappingURL=breaking-changes-registry.js.map

File diff suppressed because one or more lines are too long

24
dist/services/confidence-scorer.d.ts vendored Normal file
View File

@@ -0,0 +1,24 @@
export interface ConfidenceScore {
value: number;
reason: string;
factors: ConfidenceFactor[];
}
export interface ConfidenceFactor {
name: string;
weight: number;
matched: boolean;
description: string;
}
export declare class ConfidenceScorer {
static scoreResourceLocatorRecommendation(fieldName: string, nodeType: string, value: string): ConfidenceScore;
private static readonly EXACT_FIELD_MAPPINGS;
private static checkExactFieldMatch;
private static readonly FIELD_PATTERNS;
private static checkFieldPattern;
private static checkValuePattern;
private static readonly RESOURCE_HEAVY_NODES;
private static checkNodeCategory;
static getConfidenceLevel(score: number): 'high' | 'medium' | 'low' | 'very-low';
static shouldApplyRecommendation(score: number, threshold?: 'strict' | 'normal' | 'relaxed'): boolean;
}
//# sourceMappingURL=confidence-scorer.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"confidence-scorer.d.ts","sourceRoot":"","sources":["../../src/services/confidence-scorer.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,gBAAgB,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,gBAAgB;IAI3B,MAAM,CAAC,kCAAkC,CACvC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACZ,eAAe;IAyElB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,oBAAoB,CAO1C;IAEF,OAAO,CAAC,MAAM,CAAC,oBAAoB;IAenC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CASpC;IAEF,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAOhC,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAsBhC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,oBAAoB,CAU1C;IAEF,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAWhC,MAAM,CAAC,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,GAAG,UAAU;IAUhF,MAAM,CAAC,yBAAyB,CAC9B,KAAK,EAAE,MAAM,EACb,SAAS,GAAE,QAAQ,GAAG,QAAQ,GAAG,SAAoB,GACpD,OAAO;CASX"}

139
dist/services/confidence-scorer.js vendored Normal file
View File

@@ -0,0 +1,139 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConfidenceScorer = void 0;
class ConfidenceScorer {
static scoreResourceLocatorRecommendation(fieldName, nodeType, value) {
const factors = [];
let totalWeight = 0;
let matchedWeight = 0;
const exactFieldMatch = this.checkExactFieldMatch(fieldName, nodeType);
factors.push({
name: 'exact-field-match',
weight: 0.5,
matched: exactFieldMatch,
description: `Field name '${fieldName}' is known to use resource locator in ${nodeType}`
});
const patternMatch = this.checkFieldPattern(fieldName);
factors.push({
name: 'field-pattern',
weight: 0.3,
matched: patternMatch,
description: `Field name '${fieldName}' matches common resource locator patterns`
});
const valuePattern = this.checkValuePattern(value);
factors.push({
name: 'value-pattern',
weight: 0.1,
matched: valuePattern,
description: 'Value contains patterns typical of resource identifiers'
});
const nodeCategory = this.checkNodeCategory(nodeType);
factors.push({
name: 'node-category',
weight: 0.1,
matched: nodeCategory,
description: `Node type '${nodeType}' typically uses resource locators`
});
for (const factor of factors) {
totalWeight += factor.weight;
if (factor.matched) {
matchedWeight += factor.weight;
}
}
const score = totalWeight > 0 ? matchedWeight / totalWeight : 0;
let reason;
if (score >= 0.8) {
reason = 'High confidence: Multiple strong indicators suggest resource locator format';
}
else if (score >= 0.5) {
reason = 'Medium confidence: Some indicators suggest resource locator format';
}
else if (score >= 0.3) {
reason = 'Low confidence: Weak indicators for resource locator format';
}
else {
reason = 'Very low confidence: Minimal evidence for resource locator format';
}
return {
value: score,
reason,
factors
};
}
static checkExactFieldMatch(fieldName, nodeType) {
const nodeBase = nodeType.split('.').pop()?.toLowerCase() || '';
for (const [pattern, fields] of Object.entries(this.EXACT_FIELD_MAPPINGS)) {
if (nodeBase === pattern || nodeBase.startsWith(`${pattern}-`)) {
return fields.includes(fieldName);
}
}
return false;
}
static checkFieldPattern(fieldName) {
return this.FIELD_PATTERNS.some(pattern => pattern.test(fieldName));
}
static checkValuePattern(value) {
const content = value.startsWith('=') ? value.substring(1) : value;
if (!content.includes('{{') || !content.includes('}}')) {
return false;
}
const patterns = [
/\{\{.*\.(id|Id|ID|key|Key|name|Name|path|Path|url|Url|uri|Uri).*\}\}/i,
/\{\{.*_(id|Id|ID|key|Key|name|Name|path|Path|url|Url|uri|Uri).*\}\}/i,
/\{\{.*(id|Id|ID|key|Key|name|Name|path|Path|url|Url|uri|Uri).*\}\}/i
];
return patterns.some(pattern => pattern.test(content));
}
static checkNodeCategory(nodeType) {
const nodeBase = nodeType.split('.').pop()?.toLowerCase() || '';
return this.RESOURCE_HEAVY_NODES.some(category => nodeBase.includes(category));
}
static getConfidenceLevel(score) {
if (score >= 0.8)
return 'high';
if (score >= 0.5)
return 'medium';
if (score >= 0.3)
return 'low';
return 'very-low';
}
static shouldApplyRecommendation(score, threshold = 'normal') {
const thresholds = {
strict: 0.8,
normal: 0.5,
relaxed: 0.3
};
return score >= thresholds[threshold];
}
}
exports.ConfidenceScorer = ConfidenceScorer;
ConfidenceScorer.EXACT_FIELD_MAPPINGS = {
'github': ['owner', 'repository', 'user', 'organization'],
'googlesheets': ['sheetId', 'documentId', 'spreadsheetId'],
'googledrive': ['fileId', 'folderId', 'driveId'],
'slack': ['channel', 'user', 'channelId', 'userId'],
'notion': ['databaseId', 'pageId', 'blockId'],
'airtable': ['baseId', 'tableId', 'viewId']
};
ConfidenceScorer.FIELD_PATTERNS = [
/^.*Id$/i,
/^.*Ids$/i,
/^.*Key$/i,
/^.*Name$/i,
/^.*Path$/i,
/^.*Url$/i,
/^.*Uri$/i,
/^(table|database|collection|bucket|folder|file|document|sheet|board|project|issue|user|channel|team|organization|repository|owner)$/i
];
ConfidenceScorer.RESOURCE_HEAVY_NODES = [
'github', 'gitlab', 'bitbucket',
'googlesheets', 'googledrive', 'dropbox',
'slack', 'discord', 'telegram',
'notion', 'airtable', 'baserow',
'jira', 'asana', 'trello', 'monday',
'salesforce', 'hubspot', 'pipedrive',
'stripe', 'paypal', 'square',
'aws', 'gcp', 'azure',
'mysql', 'postgres', 'mongodb', 'redis'
];
//# sourceMappingURL=confidence-scorer.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"confidence-scorer.js","sourceRoot":"","sources":["../../src/services/confidence-scorer.ts"],"names":[],"mappings":";;;AAoBA,MAAa,gBAAgB;IAI3B,MAAM,CAAC,kCAAkC,CACvC,SAAiB,EACjB,QAAgB,EAChB,KAAa;QAEb,MAAM,OAAO,GAAuB,EAAE,CAAC;QACvC,IAAI,WAAW,GAAG,CAAC,CAAC;QACpB,IAAI,aAAa,GAAG,CAAC,CAAC;QAGtB,MAAM,eAAe,GAAG,IAAI,CAAC,oBAAoB,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QACvE,OAAO,CAAC,IAAI,CAAC;YACX,IAAI,EAAE,mBAAmB;YACzB,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,eAAe;YACxB,WAAW,EAAE,eAAe,SAAS,yCAAyC,QAAQ,EAAE;SACzF,CAAC,CAAC;QAGH,MAAM,YAAY,GAAG,IAAI,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;QACvD,OAAO,CAAC,IAAI,CAAC;YACX,IAAI,EAAE,eAAe;YACrB,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,YAAY;YACrB,WAAW,EAAE,eAAe,SAAS,4CAA4C;SAClF,CAAC,CAAC;QAGH,MAAM,YAAY,GAAG,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC;QACnD,OAAO,CAAC,IAAI,CAAC;YACX,IAAI,EAAE,eAAe;YACrB,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,YAAY;YACrB,WAAW,EAAE,yDAAyD;SACvE,CAAC,CAAC;QAGH,MAAM,YAAY,GAAG,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC;QACtD,OAAO,CAAC,IAAI,CAAC;YACX,IAAI,EAAE,eAAe;YACrB,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,YAAY;YACrB,WAAW,EAAE,cAAc,QAAQ,oCAAoC;SACxE,CAAC,CAAC;QAGH,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,WAAW,IAAI,MAAM,CAAC,MAAM,CAAC;YAC7B,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;gBACnB,aAAa,IAAI,MAAM,CAAC,MAAM,CAAC;YACjC,CAAC;QACH,CAAC;QAED,MAAM,KAAK,GAAG,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC,aAAa,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;QAGhE,IAAI,MAAc,CAAC;QACnB,IAAI,KAAK,IAAI,GAAG,EAAE,CAAC;YACjB,MAAM,GAAG,6EAA6E,CAAC;QACzF,CAAC;aAAM,IAAI,KAAK,IAAI,GAAG,EAAE,CAAC;YACxB,MAAM,GAAG,oEAAoE,CAAC;QAChF,CAAC;aAAM,IAAI,KAAK,IAAI,GAAG,EAAE,CAAC;YACxB,MAAM,GAAG,6DAA6D,CAAC;QACzE,CAAC;aAAM,CAAC;YACN,MAAM,GAAG,mEAAmE,CAAC;QAC/E,CAAC;QAED,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,MAAM;YACN,OAAO;SACR,CAAC;IACJ,CAAC;IAcO,MAAM,CAAC,oBAAoB,CAAC,SAAiB,EAAE,QAAgB;QACrE,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;QAEhE,KAAK,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,oBAAoB,CAAC,EAAE,CAAC;YAC1E,IAAI,QAAQ,KAAK,OAAO,IAAI,QAAQ,CAAC,UAAU,CAAC,GAAG,OAAO,GAAG,CAAC,EAAE,CAAC;gBAC/D,OAAO,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;YACpC,CAAC;QACH,CAAC;QAED,OAAO,KAAK,CAAC;IACf,CAAC;IAgBO,MAAM,CAAC,iBAAiB,CAAC,SAAiB;QAChD,OAAO,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;IACtE,CAAC;IAKO,MAAM,CAAC,iBAAiB,CAAC,KAAa;QAE5C,MAAM,OAAO,GAAG,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;QAGnE,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YACvD,OAAO,KAAK,CAAC;QACf,CAAC;QAGD,MAAM,QAAQ,GAAG;YACf,uEAAuE;YACvE,sEAAsE;YACtE,qEAAqE;SACtE,CAAC;QAEF,OAAO,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;IACzD,CAAC;IAiBO,MAAM,CAAC,iBAAiB,CAAC,QAAgB;QAC/C,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;QAEhE,OAAO,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAC/C,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAC5B,CAAC;IACJ,CAAC;IAKD,MAAM,CAAC,kBAAkB,CAAC,KAAa;QACrC,IAAI,KAAK,IAAI,GAAG;YAAE,OAAO,MAAM,CAAC;QAChC,IAAI,KAAK,IAAI,GAAG;YAAE,OAAO,QAAQ,CAAC;QAClC,IAAI,KAAK,IAAI,GAAG;YAAE,OAAO,KAAK,CAAC;QAC/B,OAAO,UAAU,CAAC;IACpB,CAAC;IAKD,MAAM,CAAC,yBAAyB,CAC9B,KAAa,EACb,YAA6C,QAAQ;QAErD,MAAM,UAAU,GAAG;YACjB,MAAM,EAAE,GAAG;YACX,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,GAAG;SACb,CAAC;QAEF,OAAO,KAAK,IAAI,UAAU,CAAC,SAAS,CAAC,CAAC;IACxC,CAAC;;AA7LH,4CA8LC;AA7GyB,qCAAoB,GAA6B;IACvE,QAAQ,EAAE,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,cAAc,CAAC;IACzD,cAAc,EAAE,CAAC,SAAS,EAAE,YAAY,EAAE,eAAe,CAAC;IAC1D,aAAa,EAAE,CAAC,QAAQ,EAAE,UAAU,EAAE,SAAS,CAAC;IAChD,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,CAAC;IACnD,QAAQ,EAAE,CAAC,YAAY,EAAE,QAAQ,EAAE,SAAS,CAAC;IAC7C,UAAU,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,QAAQ,CAAC;CAC5C,CAAC;AAiBsB,+BAAc,GAAG;IACvC,SAAS;IACT,UAAU;IACV,UAAU;IACV,WAAW;IACX,WAAW;IACX,UAAU;IACV,UAAU;IACV,sIAAsI;CACvI,CAAC;AA+BsB,qCAAoB,GAAG;IAC7C,QAAQ,EAAE,QAAQ,EAAE,WAAW;IAC/B,cAAc,EAAE,aAAa,EAAE,SAAS;IACxC,OAAO,EAAE,SAAS,EAAE,UAAU;IAC9B,QAAQ,EAAE,UAAU,EAAE,SAAS;IAC/B,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ;IACnC,YAAY,EAAE,SAAS,EAAE,WAAW;IACpC,QAAQ,EAAE,QAAQ,EAAE,QAAQ;IAC5B,KAAK,EAAE,KAAK,EAAE,OAAO;IACrB,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO;CACxC,CAAC"}

47
dist/services/config-validator.d.ts vendored Normal file
View File

@@ -0,0 +1,47 @@
export interface ValidationResult {
valid: boolean;
errors: ValidationError[];
warnings: ValidationWarning[];
suggestions: string[];
visibleProperties: string[];
hiddenProperties: string[];
autofix?: Record<string, any>;
}
export interface ValidationError {
type: 'missing_required' | 'invalid_type' | 'invalid_value' | 'incompatible' | 'invalid_configuration' | 'syntax_error';
property: string;
message: string;
fix?: string;
suggestion?: string;
}
export interface ValidationWarning {
type: 'missing_common' | 'deprecated' | 'inefficient' | 'security' | 'best_practice' | 'invalid_value';
property?: string;
message: string;
suggestion?: string;
}
export declare class ConfigValidator {
private static readonly UI_ONLY_TYPES;
static validate(nodeType: string, config: Record<string, any>, properties: any[], userProvidedKeys?: Set<string>): ValidationResult;
static validateBatch(configs: Array<{
nodeType: string;
config: Record<string, any>;
properties: any[];
}>): ValidationResult[];
private static checkRequiredProperties;
private static getPropertyVisibility;
protected static isPropertyVisible(prop: any, config: Record<string, any>): boolean;
private static validatePropertyTypes;
private static performNodeSpecificValidation;
private static validateHttpRequest;
private static validateWebhook;
private static validateDatabase;
private static validateCode;
private static checkCommonIssues;
private static performSecurityChecks;
private static getVisibilityRequirement;
private static validateJavaScriptSyntax;
private static validatePythonSyntax;
private static validateN8nCodePatterns;
}
//# sourceMappingURL=config-validator.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"config-validator.d.ts","sourceRoot":"","sources":["../../src/services/config-validator.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,kBAAkB,GAAG,cAAc,GAAG,eAAe,GAAG,cAAc,GAAG,uBAAuB,GAAG,cAAc,CAAC;IACxH,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,gBAAgB,GAAG,YAAY,GAAG,aAAa,GAAG,UAAU,GAAG,eAAe,GAAG,eAAe,CAAC;IACvG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,eAAe;IAI1B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAA4C;IAKjF,MAAM,CAAC,QAAQ,CACb,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,UAAU,EAAE,GAAG,EAAE,EACjB,gBAAgB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAC7B,gBAAgB;IAsDnB,MAAM,CAAC,aAAa,CAClB,OAAO,EAAE,KAAK,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAC5B,UAAU,EAAE,GAAG,EAAE,CAAC;KACnB,CAAC,GACD,gBAAgB,EAAE;IASrB,OAAO,CAAC,MAAM,CAAC,uBAAuB;IA0CtC,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAqBpC,SAAS,CAAC,MAAM,CAAC,iBAAiB,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO;IAiCnF,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAoIpC,OAAO,CAAC,MAAM,CAAC,6BAA6B;IA+B5C,OAAO,CAAC,MAAM,CAAC,mBAAmB;IAoElC,OAAO,CAAC,MAAM,CAAC,eAAe;IAc9B,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAoC/B,OAAO,CAAC,MAAM,CAAC,YAAY;IAyC3B,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAgEhC,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAmCpC,OAAO,CAAC,MAAM,CAAC,wBAAwB;IA6BvC,OAAO,CAAC,MAAM,CAAC,wBAAwB;IA4CvC,OAAO,CAAC,MAAM,CAAC,oBAAoB;IAgEnC,OAAO,CAAC,MAAM,CAAC,uBAAuB;CAmOvC"}

671
dist/services/config-validator.js vendored Normal file
View File

@@ -0,0 +1,671 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConfigValidator = void 0;
const expression_utils_js_1 = require("../utils/expression-utils.js");
class ConfigValidator {
static validate(nodeType, config, properties, userProvidedKeys) {
if (!config || typeof config !== 'object') {
throw new TypeError('Config must be a non-null object');
}
if (!properties || !Array.isArray(properties)) {
throw new TypeError('Properties must be a non-null array');
}
const errors = [];
const warnings = [];
const suggestions = [];
const visibleProperties = [];
const hiddenProperties = [];
const autofix = {};
this.checkRequiredProperties(properties, config, errors);
const { visible, hidden } = this.getPropertyVisibility(properties, config);
visibleProperties.push(...visible);
hiddenProperties.push(...hidden);
this.validatePropertyTypes(properties, config, errors);
this.performNodeSpecificValidation(nodeType, config, errors, warnings, suggestions, autofix);
this.checkCommonIssues(nodeType, config, properties, warnings, suggestions, userProvidedKeys);
this.performSecurityChecks(nodeType, config, warnings);
return {
valid: errors.length === 0,
errors,
warnings,
suggestions,
visibleProperties,
hiddenProperties,
autofix: Object.keys(autofix).length > 0 ? autofix : undefined
};
}
static validateBatch(configs) {
return configs.map(({ nodeType, config, properties }) => this.validate(nodeType, config, properties));
}
static checkRequiredProperties(properties, config, errors) {
for (const prop of properties) {
if (!prop || !prop.name)
continue;
if (prop.required) {
const value = config[prop.name];
if (!(prop.name in config)) {
errors.push({
type: 'missing_required',
property: prop.name,
message: `Required property '${prop.displayName || prop.name}' is missing`,
fix: `Add ${prop.name} to your configuration`
});
}
else if (value === null || value === undefined) {
errors.push({
type: 'invalid_type',
property: prop.name,
message: `Required property '${prop.displayName || prop.name}' cannot be null or undefined`,
fix: `Provide a valid value for ${prop.name}`
});
}
else if (typeof value === 'string' && value.trim() === '') {
errors.push({
type: 'missing_required',
property: prop.name,
message: `Required property '${prop.displayName || prop.name}' cannot be empty`,
fix: `Provide a valid value for ${prop.name}`
});
}
}
}
}
static getPropertyVisibility(properties, config) {
const visible = [];
const hidden = [];
for (const prop of properties) {
if (this.isPropertyVisible(prop, config)) {
visible.push(prop.name);
}
else {
hidden.push(prop.name);
}
}
return { visible, hidden };
}
static isPropertyVisible(prop, config) {
if (!prop.displayOptions)
return true;
if (prop.displayOptions.show) {
for (const [key, values] of Object.entries(prop.displayOptions.show)) {
const configValue = config[key];
const expectedValues = Array.isArray(values) ? values : [values];
if (!expectedValues.includes(configValue)) {
return false;
}
}
}
if (prop.displayOptions.hide) {
for (const [key, values] of Object.entries(prop.displayOptions.hide)) {
const configValue = config[key];
const expectedValues = Array.isArray(values) ? values : [values];
if (expectedValues.includes(configValue)) {
return false;
}
}
}
return true;
}
static validatePropertyTypes(properties, config, errors) {
for (const [key, value] of Object.entries(config)) {
const prop = properties.find(p => p.name === key);
if (!prop)
continue;
if (prop.type === 'string' && typeof value !== 'string') {
errors.push({
type: 'invalid_type',
property: key,
message: `Property '${key}' must be a string, got ${typeof value}`,
fix: `Change ${key} to a string value`
});
}
else if (prop.type === 'number' && typeof value !== 'number') {
errors.push({
type: 'invalid_type',
property: key,
message: `Property '${key}' must be a number, got ${typeof value}`,
fix: `Change ${key} to a number`
});
}
else if (prop.type === 'boolean' && typeof value !== 'boolean') {
errors.push({
type: 'invalid_type',
property: key,
message: `Property '${key}' must be a boolean, got ${typeof value}`,
fix: `Change ${key} to true or false`
});
}
else if (prop.type === 'resourceLocator') {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
const fixValue = typeof value === 'string' ? value : JSON.stringify(value);
errors.push({
type: 'invalid_type',
property: key,
message: `Property '${key}' is a resourceLocator and must be an object with 'mode' and 'value' properties, got ${typeof value}`,
fix: `Change ${key} to { mode: "list", value: ${JSON.stringify(fixValue)} } or { mode: "id", value: ${JSON.stringify(fixValue)} }`
});
}
else {
if (!value.mode) {
errors.push({
type: 'missing_required',
property: `${key}.mode`,
message: `resourceLocator '${key}' is missing required property 'mode'`,
fix: `Add mode property: { mode: "list", value: ${JSON.stringify(value.value || '')} }`
});
}
else if (typeof value.mode !== 'string') {
errors.push({
type: 'invalid_type',
property: `${key}.mode`,
message: `resourceLocator '${key}.mode' must be a string, got ${typeof value.mode}`,
fix: `Set mode to a valid string value`
});
}
else if (prop.modes) {
const modes = prop.modes;
if (!modes || typeof modes !== 'object') {
continue;
}
let allowedModes = [];
if (Array.isArray(modes)) {
allowedModes = modes
.map(m => (typeof m === 'object' && m !== null) ? m.name : m)
.filter(m => typeof m === 'string' && m.length > 0);
}
else {
allowedModes = Object.keys(modes).filter(k => k.length > 0);
}
if (allowedModes.length > 0 && !allowedModes.includes(value.mode)) {
errors.push({
type: 'invalid_value',
property: `${key}.mode`,
message: `resourceLocator '${key}.mode' must be one of [${allowedModes.join(', ')}], got '${value.mode}'`,
fix: `Change mode to one of: ${allowedModes.join(', ')}`
});
}
}
if (value.value === undefined) {
errors.push({
type: 'missing_required',
property: `${key}.value`,
message: `resourceLocator '${key}' is missing required property 'value'`,
fix: `Add value property to specify the ${prop.displayName || key}`
});
}
}
}
if (prop.type === 'options' && prop.options) {
const validValues = prop.options.map((opt) => typeof opt === 'string' ? opt : opt.value);
if (!validValues.includes(value)) {
errors.push({
type: 'invalid_value',
property: key,
message: `Invalid value for '${key}'. Must be one of: ${validValues.join(', ')}`,
fix: `Change ${key} to one of the valid options`
});
}
}
}
}
static performNodeSpecificValidation(nodeType, config, errors, warnings, suggestions, autofix) {
switch (nodeType) {
case 'nodes-base.httpRequest':
this.validateHttpRequest(config, errors, warnings, suggestions, autofix);
break;
case 'nodes-base.webhook':
this.validateWebhook(config, warnings, suggestions);
break;
case 'nodes-base.postgres':
case 'nodes-base.mysql':
this.validateDatabase(config, warnings, suggestions);
break;
case 'nodes-base.code':
this.validateCode(config, errors, warnings);
break;
}
}
static validateHttpRequest(config, errors, warnings, suggestions, autofix) {
if (config.url && typeof config.url === 'string') {
if (!(0, expression_utils_js_1.shouldSkipLiteralValidation)(config.url)) {
if (!config.url.startsWith('http://') && !config.url.startsWith('https://')) {
errors.push({
type: 'invalid_value',
property: 'url',
message: 'URL must start with http:// or https://',
fix: 'Add https:// to the beginning of your URL'
});
}
}
}
if (['POST', 'PUT', 'PATCH'].includes(config.method) && !config.sendBody) {
warnings.push({
type: 'missing_common',
property: 'sendBody',
message: `${config.method} requests typically send a body`,
suggestion: 'Set sendBody=true and configure the body content'
});
autofix.sendBody = true;
autofix.contentType = 'json';
}
if (!config.authentication || config.authentication === 'none') {
if (config.url?.includes('api.') || config.url?.includes('/api/')) {
warnings.push({
type: 'security',
message: 'API endpoints typically require authentication',
suggestion: 'Consider setting authentication if the API requires it'
});
}
}
if (config.sendBody && config.contentType === 'json' && config.jsonBody) {
if (!(0, expression_utils_js_1.shouldSkipLiteralValidation)(config.jsonBody)) {
try {
JSON.parse(config.jsonBody);
}
catch (e) {
const errorMsg = e instanceof Error ? e.message : 'Unknown parsing error';
errors.push({
type: 'invalid_value',
property: 'jsonBody',
message: `jsonBody contains invalid JSON: ${errorMsg}`,
fix: 'Fix JSON syntax error and ensure valid JSON format'
});
}
}
}
}
static validateWebhook(config, warnings, suggestions) {
if (config.responseMode === 'responseNode' && !config.responseData) {
suggestions.push('When using responseMode=responseNode, add a "Respond to Webhook" node to send custom responses');
}
}
static validateDatabase(config, warnings, suggestions) {
if (config.query) {
const query = config.query.toLowerCase();
if (query.includes('${') || query.includes('{{')) {
warnings.push({
type: 'security',
message: 'Query contains template expressions that might be vulnerable to SQL injection',
suggestion: 'Use parameterized queries with additionalFields.queryParams instead'
});
}
if (query.includes('delete') && !query.includes('where')) {
warnings.push({
type: 'security',
message: 'DELETE query without WHERE clause will delete all records',
suggestion: 'Add a WHERE clause to limit the deletion'
});
}
if (query.includes('select *')) {
suggestions.push('Consider selecting specific columns instead of * for better performance');
}
}
}
static validateCode(config, errors, warnings) {
const codeField = config.language === 'python' ? 'pythonCode' : 'jsCode';
const code = config[codeField];
if (!code || code.trim() === '') {
errors.push({
type: 'missing_required',
property: codeField,
message: 'Code cannot be empty',
fix: 'Add your code logic'
});
return;
}
if (code?.includes('eval(') || code?.includes('exec(')) {
warnings.push({
type: 'security',
message: 'Code contains eval/exec which can be a security risk',
suggestion: 'Avoid using eval/exec with untrusted input'
});
}
if (config.language === 'python') {
this.validatePythonSyntax(code, errors, warnings);
}
else {
this.validateJavaScriptSyntax(code, errors, warnings);
}
this.validateN8nCodePatterns(code, config.language || 'javascript', errors, warnings);
}
static checkCommonIssues(nodeType, config, properties, warnings, suggestions, userProvidedKeys) {
if (nodeType === 'nodes-base.code') {
return;
}
const visibleProps = properties.filter(p => this.isPropertyVisible(p, config));
const configuredKeys = Object.keys(config);
for (const key of configuredKeys) {
if (key === '@version' || key.startsWith('_')) {
continue;
}
if (userProvidedKeys && !userProvidedKeys.has(key)) {
continue;
}
const prop = properties.find(p => p.name === key);
if (prop && this.UI_ONLY_TYPES.includes(prop.type)) {
continue;
}
if (!visibleProps.find(p => p.name === key)) {
const visibilityReq = this.getVisibilityRequirement(prop, config);
warnings.push({
type: 'inefficient',
property: key,
message: `Property '${prop?.displayName || key}' won't be used - not visible with current settings`,
suggestion: visibilityReq || 'Remove this property or adjust other settings to make it visible'
});
}
}
const commonProps = ['authentication', 'errorHandling', 'timeout'];
for (const prop of commonProps) {
const propDef = properties.find(p => p.name === prop);
if (propDef && this.isPropertyVisible(propDef, config) && !(prop in config)) {
suggestions.push(`Consider setting '${prop}' for better control`);
}
}
}
static performSecurityChecks(nodeType, config, warnings) {
const sensitivePatterns = [
/api[_-]?key/i,
/password/i,
/secret/i,
/token/i,
/credential/i
];
for (const [key, value] of Object.entries(config)) {
if (typeof value === 'string') {
for (const pattern of sensitivePatterns) {
if (pattern.test(key) && value.length > 0 && !value.includes('{{')) {
warnings.push({
type: 'security',
property: key,
message: `Hardcoded ${key} detected`,
suggestion: 'Use n8n credentials or expressions instead of hardcoding sensitive values'
});
break;
}
}
}
}
}
static getVisibilityRequirement(prop, config) {
if (!prop || !prop.displayOptions?.show) {
return undefined;
}
const requirements = [];
for (const [field, values] of Object.entries(prop.displayOptions.show)) {
const expectedValues = Array.isArray(values) ? values : [values];
const currentValue = config[field];
if (!expectedValues.includes(currentValue)) {
const valueStr = expectedValues.length === 1
? `"${expectedValues[0]}"`
: expectedValues.map(v => `"${v}"`).join(' or ');
requirements.push(`${field}=${valueStr}`);
}
}
if (requirements.length === 0) {
return undefined;
}
return `Requires: ${requirements.join(', ')}`;
}
static validateJavaScriptSyntax(code, errors, warnings) {
const openBraces = (code.match(/\{/g) || []).length;
const closeBraces = (code.match(/\}/g) || []).length;
if (openBraces !== closeBraces) {
errors.push({
type: 'invalid_value',
property: 'jsCode',
message: 'Unbalanced braces detected',
fix: 'Check that all { have matching }'
});
}
const openParens = (code.match(/\(/g) || []).length;
const closeParens = (code.match(/\)/g) || []).length;
if (openParens !== closeParens) {
errors.push({
type: 'invalid_value',
property: 'jsCode',
message: 'Unbalanced parentheses detected',
fix: 'Check that all ( have matching )'
});
}
const stringMatches = code.match(/(["'`])(?:(?=(\\?))\2.)*?\1/g) || [];
const quotesInStrings = stringMatches.join('').match(/["'`]/g)?.length || 0;
const totalQuotes = (code.match(/["'`]/g) || []).length;
if ((totalQuotes - quotesInStrings) % 2 !== 0) {
warnings.push({
type: 'inefficient',
message: 'Possible unterminated string detected',
suggestion: 'Check that all strings are properly closed'
});
}
}
static validatePythonSyntax(code, errors, warnings) {
const lines = code.split('\n');
const indentTypes = new Set();
lines.forEach(line => {
const indent = line.match(/^(\s+)/);
if (indent) {
if (indent[1].includes('\t'))
indentTypes.add('tabs');
if (indent[1].includes(' '))
indentTypes.add('spaces');
}
});
if (indentTypes.size > 1) {
errors.push({
type: 'syntax_error',
property: 'pythonCode',
message: 'Mixed indentation (tabs and spaces)',
fix: 'Use either tabs or spaces consistently, not both'
});
}
const openSquare = (code.match(/\[/g) || []).length;
const closeSquare = (code.match(/\]/g) || []).length;
if (openSquare !== closeSquare) {
errors.push({
type: 'syntax_error',
property: 'pythonCode',
message: 'Unmatched bracket - missing ] or extra [',
fix: 'Check that all [ have matching ]'
});
}
const openCurly = (code.match(/\{/g) || []).length;
const closeCurly = (code.match(/\}/g) || []).length;
if (openCurly !== closeCurly) {
errors.push({
type: 'syntax_error',
property: 'pythonCode',
message: 'Unmatched bracket - missing } or extra {',
fix: 'Check that all { have matching }'
});
}
const controlStructures = /^\s*(if|elif|else|for|while|def|class|try|except|finally|with)\s+.*[^:]\s*$/gm;
if (controlStructures.test(code)) {
warnings.push({
type: 'inefficient',
message: 'Missing colon after control structure',
suggestion: 'Add : at the end of if/for/def/class statements'
});
}
}
static validateN8nCodePatterns(code, language, errors, warnings) {
const hasReturn = language === 'python'
? /return\s+/.test(code)
: /return\s+/.test(code);
if (!hasReturn) {
warnings.push({
type: 'missing_common',
message: 'No return statement found',
suggestion: 'Code node must return data. Example: return [{json: {result: "success"}}]'
});
}
if (language === 'javascript' && hasReturn) {
if (/return\s+items\s*;/.test(code) && !code.includes('.map') && !code.includes('json:')) {
warnings.push({
type: 'best_practice',
message: 'Returning items directly - ensure each item has {json: ...} structure',
suggestion: 'If modifying items, use: return items.map(item => ({json: {...item.json, newField: "value"}}))'
});
}
if (/return\s+{[^}]+}\s*;/.test(code) && !code.includes('[') && !code.includes(']')) {
warnings.push({
type: 'invalid_value',
message: 'Return value must be an array',
suggestion: 'Wrap your return object in an array: return [{json: {your: "data"}}]'
});
}
if (/return\s+\[['"`]/.test(code) || /return\s+\[\d/.test(code)) {
warnings.push({
type: 'invalid_value',
message: 'Items must be objects with json property',
suggestion: 'Use format: return [{json: {value: "data"}}] not return ["data"]'
});
}
}
if (language === 'python' && hasReturn) {
if (code.includes('result = {"data": "value"}')) {
console.log('DEBUG: Processing Python code with result variable');
console.log('DEBUG: Language:', language);
console.log('DEBUG: Has return:', hasReturn);
}
if (/return\s+items\s*$/.test(code) && !code.includes('json') && !code.includes('dict')) {
warnings.push({
type: 'best_practice',
message: 'Returning items directly - ensure each item is a dict with "json" key',
suggestion: 'Use: return [{"json": item.json} for item in items]'
});
}
if (/return\s+{['"]/.test(code) && !code.includes('[') && !code.includes(']')) {
warnings.push({
type: 'invalid_value',
message: 'Return value must be a list',
suggestion: 'Wrap your return dict in a list: return [{"json": {"your": "data"}}]'
});
}
if (/return\s+(?!.*\[).*{(?!.*["']json["'])/.test(code)) {
warnings.push({
type: 'invalid_value',
message: 'Must return array of objects with json key',
suggestion: 'Use format: return [{"json": {"data": "value"}}]'
});
}
const returnMatch = code.match(/return\s+(\w+)\s*(?:#|$)/m);
if (returnMatch) {
const varName = returnMatch[1];
const assignmentRegex = new RegExp(`${varName}\\s*=\\s*{[^}]+}`, 'm');
if (assignmentRegex.test(code) && !new RegExp(`${varName}\\s*=\\s*\\[`).test(code)) {
warnings.push({
type: 'invalid_value',
message: 'Must return array of objects with json key',
suggestion: `Wrap ${varName} in a list with json key: return [{"json": ${varName}}]`
});
}
}
}
if (language === 'javascript') {
if (!code.includes('items') && !code.includes('$input') && !code.includes('$json')) {
warnings.push({
type: 'missing_common',
message: 'Code doesn\'t reference input data',
suggestion: 'Access input with: items, $input.all(), or $json (in single-item mode)'
});
}
if (code.includes('$json') && !code.includes('mode')) {
warnings.push({
type: 'best_practice',
message: '$json only works in "Run Once for Each Item" mode',
suggestion: 'For all items mode, use: items[0].json or loop through items'
});
}
const commonVars = ['$node', '$workflow', '$execution', '$prevNode', 'DateTime', 'jmespath'];
const usedVars = commonVars.filter(v => code.includes(v));
if (code.includes('$helpers.getWorkflowStaticData')) {
if (/\$helpers\.getWorkflowStaticData(?!\s*\()/.test(code)) {
errors.push({
type: 'invalid_value',
property: 'jsCode',
message: 'getWorkflowStaticData requires parentheses: $helpers.getWorkflowStaticData()',
fix: 'Add parentheses: $helpers.getWorkflowStaticData()'
});
}
else {
warnings.push({
type: 'invalid_value',
message: '$helpers.getWorkflowStaticData() is incorrect - causes "$helpers is not defined" error',
suggestion: 'Use $getWorkflowStaticData() as a standalone function (no $helpers prefix)'
});
}
}
if (code.includes('$helpers') && !code.includes('typeof $helpers')) {
warnings.push({
type: 'best_practice',
message: '$helpers is only available in Code nodes with mode="runOnceForEachItem"',
suggestion: 'Check availability first: if (typeof $helpers !== "undefined" && $helpers.httpRequest) { ... }'
});
}
if ((code.includes('fetch(') || code.includes('Promise') || code.includes('.then(')) && !code.includes('await')) {
warnings.push({
type: 'best_practice',
message: 'Async operation without await - will return a Promise instead of actual data',
suggestion: 'Use await with async operations: const result = await fetch(...);'
});
}
if ((code.includes('crypto.') || code.includes('randomBytes') || code.includes('randomUUID')) && !code.includes('require')) {
warnings.push({
type: 'invalid_value',
message: 'Using crypto without require statement',
suggestion: 'Add: const crypto = require("crypto"); at the beginning (ignore editor warnings)'
});
}
if (code.includes('console.log')) {
warnings.push({
type: 'best_practice',
message: 'console.log output appears in n8n execution logs',
suggestion: 'Remove console.log statements in production or use them sparingly'
});
}
}
else if (language === 'python') {
if (!code.includes('items') && !code.includes('_input')) {
warnings.push({
type: 'missing_common',
message: 'Code doesn\'t reference input items',
suggestion: 'Access input data with: items variable'
});
}
if (code.includes('print(')) {
warnings.push({
type: 'best_practice',
message: 'print() output appears in n8n execution logs',
suggestion: 'Remove print statements in production or use them sparingly'
});
}
if (code.includes('import requests') || code.includes('import pandas')) {
warnings.push({
type: 'invalid_value',
message: 'External libraries not available in Code node',
suggestion: 'Only Python standard library is available. For HTTP requests, use JavaScript with $helpers.httpRequest'
});
}
}
if (/while\s*\(\s*true\s*\)|while\s+True:/.test(code)) {
warnings.push({
type: 'security',
message: 'Infinite loop detected',
suggestion: 'Add a break condition or use a for loop with limits'
});
}
if (!code.includes('try') && !code.includes('catch') && !code.includes('except')) {
if (code.length > 200) {
warnings.push({
type: 'best_practice',
message: 'No error handling found',
suggestion: 'Consider adding try/catch (JavaScript) or try/except (Python) for robust error handling'
});
}
}
}
}
exports.ConfigValidator = ConfigValidator;
ConfigValidator.UI_ONLY_TYPES = ['notice', 'callout', 'infoBox', 'info'];
//# sourceMappingURL=config-validator.js.map

1
dist/services/config-validator.js.map vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,54 @@
import { ConfigValidator, ValidationResult } from './config-validator';
import { NodeRepository } from '../database/node-repository';
export type ValidationMode = 'full' | 'operation' | 'minimal';
export type ValidationProfile = 'strict' | 'runtime' | 'ai-friendly' | 'minimal';
export interface EnhancedValidationResult extends ValidationResult {
mode: ValidationMode;
profile?: ValidationProfile;
operation?: {
resource?: string;
operation?: string;
action?: string;
};
examples?: Array<{
description: string;
config: Record<string, any>;
}>;
nextSteps?: string[];
}
export interface OperationContext {
resource?: string;
operation?: string;
action?: string;
mode?: string;
}
export declare class EnhancedConfigValidator extends ConfigValidator {
private static operationSimilarityService;
private static resourceSimilarityService;
private static nodeRepository;
static initializeSimilarityServices(repository: NodeRepository): void;
static validateWithMode(nodeType: string, config: Record<string, any>, properties: any[], mode?: ValidationMode, profile?: ValidationProfile): EnhancedValidationResult;
private static extractOperationContext;
private static filterPropertiesByMode;
private static applyNodeDefaults;
private static isPropertyRelevantToOperation;
private static addOperationSpecificEnhancements;
private static enhanceSlackValidation;
private static enhanceGoogleSheetsValidation;
private static enhanceHttpRequestValidation;
private static generateNextSteps;
private static deduplicateErrors;
private static shouldFilterCredentialWarning;
private static applyProfileFilters;
private static enforceErrorHandlingForProfile;
private static addErrorHandlingSuggestions;
private static validateFixedCollectionStructures;
private static validateSwitchNodeStructure;
private static validateIfNodeStructure;
private static validateFilterNodeStructure;
private static validateResourceAndOperation;
private static validateSpecialTypeStructures;
private static validateComplexTypeStructure;
private static validateFilterOperations;
}
//# sourceMappingURL=enhanced-config-validator.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"enhanced-config-validator.d.ts","sourceRoot":"","sources":["../../src/services/enhanced-config-validator.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAsC,MAAM,oBAAoB,CAAC;AAK3G,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAM7D,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,CAAC;AAC9D,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,SAAS,GAAG,aAAa,GAAG,SAAS,CAAC;AAEjF,MAAM,WAAW,wBAAyB,SAAQ,gBAAgB;IAChE,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,SAAS,CAAC,EAAE;QACV,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,QAAQ,CAAC,EAAE,KAAK,CAAC;QACf,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;KAC7B,CAAC,CAAC;IACH,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,uBAAwB,SAAQ,eAAe;IAC1D,OAAO,CAAC,MAAM,CAAC,0BAA0B,CAA2C;IACpF,OAAO,CAAC,MAAM,CAAC,yBAAyB,CAA0C;IAClF,OAAO,CAAC,MAAM,CAAC,cAAc,CAA+B;IAK5D,MAAM,CAAC,4BAA4B,CAAC,UAAU,EAAE,cAAc,GAAG,IAAI;IAQrE,MAAM,CAAC,gBAAgB,CACrB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,UAAU,EAAE,GAAG,EAAE,EACjB,IAAI,GAAE,cAA4B,EAClC,OAAO,GAAE,iBAAiC,GACzC,wBAAwB;IAqE3B,OAAO,CAAC,MAAM,CAAC,uBAAuB;IAatC,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAsCrC,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAehC,OAAO,CAAC,MAAM,CAAC,6BAA6B;IAgD5C,OAAO,CAAC,MAAM,CAAC,gCAAgC;IAgH/C,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAyBrC,OAAO,CAAC,MAAM,CAAC,6BAA6B;IAwB5C,OAAO,CAAC,MAAM,CAAC,4BAA4B;IA8D3C,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAoChC,OAAO,CAAC,MAAM,CAAC,iBAAiB;IA0BhC,OAAO,CAAC,MAAM,CAAC,6BAA6B;IAS5C,OAAO,CAAC,MAAM,CAAC,mBAAmB;IAsFlC,OAAO,CAAC,MAAM,CAAC,8BAA8B;IAyB7C,OAAO,CAAC,MAAM,CAAC,2BAA2B;IA+B1C,OAAO,CAAC,MAAM,CAAC,iCAAiC;IA+ChD,OAAO,CAAC,MAAM,CAAC,2BAA2B;IAuC1C,OAAO,CAAC,MAAM,CAAC,uBAAuB;IAmBtC,OAAO,CAAC,MAAM,CAAC,2BAA2B;IAmB1C,OAAO,CAAC,MAAM,CAAC,4BAA4B;IAiM3C,OAAO,CAAC,MAAM,CAAC,6BAA6B;IA4E5C,OAAO,CAAC,MAAM,CAAC,4BAA4B;IAyH3C,OAAO,CAAC,MAAM,CAAC,wBAAwB;CAoExC"}

View File

@@ -0,0 +1,789 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.EnhancedConfigValidator = void 0;
const config_validator_1 = require("./config-validator");
const node_specific_validators_1 = require("./node-specific-validators");
const fixed_collection_validator_1 = require("../utils/fixed-collection-validator");
const operation_similarity_service_1 = require("./operation-similarity-service");
const resource_similarity_service_1 = require("./resource-similarity-service");
const node_type_normalizer_1 = require("../utils/node-type-normalizer");
const type_structure_service_1 = require("./type-structure-service");
class EnhancedConfigValidator extends config_validator_1.ConfigValidator {
static initializeSimilarityServices(repository) {
this.nodeRepository = repository;
this.operationSimilarityService = new operation_similarity_service_1.OperationSimilarityService(repository);
this.resourceSimilarityService = new resource_similarity_service_1.ResourceSimilarityService(repository);
}
static validateWithMode(nodeType, config, properties, mode = 'operation', profile = 'ai-friendly') {
if (typeof nodeType !== 'string') {
throw new Error(`Invalid nodeType: expected string, got ${typeof nodeType}`);
}
if (!config || typeof config !== 'object') {
throw new Error(`Invalid config: expected object, got ${typeof config}`);
}
if (!Array.isArray(properties)) {
throw new Error(`Invalid properties: expected array, got ${typeof properties}`);
}
const operationContext = this.extractOperationContext(config);
const userProvidedKeys = new Set(Object.keys(config));
const { properties: filteredProperties, configWithDefaults } = this.filterPropertiesByMode(properties, config, mode, operationContext);
const baseResult = super.validate(nodeType, configWithDefaults, filteredProperties, userProvidedKeys);
const enhancedResult = {
...baseResult,
mode,
profile,
operation: operationContext,
examples: [],
nextSteps: [],
errors: baseResult.errors || [],
warnings: baseResult.warnings || [],
suggestions: baseResult.suggestions || []
};
this.applyProfileFilters(enhancedResult, profile);
this.addOperationSpecificEnhancements(nodeType, config, filteredProperties, enhancedResult);
enhancedResult.errors = this.deduplicateErrors(enhancedResult.errors);
enhancedResult.nextSteps = this.generateNextSteps(enhancedResult);
enhancedResult.valid = enhancedResult.errors.length === 0;
return enhancedResult;
}
static extractOperationContext(config) {
return {
resource: config.resource,
operation: config.operation,
action: config.action,
mode: config.mode
};
}
static filterPropertiesByMode(properties, config, mode, operation) {
const configWithDefaults = this.applyNodeDefaults(properties, config);
let filteredProperties;
switch (mode) {
case 'minimal':
filteredProperties = properties.filter(prop => prop.required && this.isPropertyVisible(prop, configWithDefaults));
break;
case 'operation':
filteredProperties = properties.filter(prop => this.isPropertyRelevantToOperation(prop, configWithDefaults, operation));
break;
case 'full':
default:
filteredProperties = properties;
break;
}
return { properties: filteredProperties, configWithDefaults };
}
static applyNodeDefaults(properties, config) {
const result = { ...config };
for (const prop of properties) {
if (prop.name && prop.default !== undefined && result[prop.name] === undefined) {
result[prop.name] = prop.default;
}
}
return result;
}
static isPropertyRelevantToOperation(prop, config, operation) {
if (!this.isPropertyVisible(prop, config)) {
return false;
}
if (!operation.resource && !operation.operation && !operation.action) {
return true;
}
if (prop.displayOptions?.show) {
const show = prop.displayOptions.show;
if (operation.resource && show.resource) {
const expectedResources = Array.isArray(show.resource) ? show.resource : [show.resource];
if (!expectedResources.includes(operation.resource)) {
return false;
}
}
if (operation.operation && show.operation) {
const expectedOps = Array.isArray(show.operation) ? show.operation : [show.operation];
if (!expectedOps.includes(operation.operation)) {
return false;
}
}
if (operation.action && show.action) {
const expectedActions = Array.isArray(show.action) ? show.action : [show.action];
if (!expectedActions.includes(operation.action)) {
return false;
}
}
}
return true;
}
static addOperationSpecificEnhancements(nodeType, config, properties, result) {
if (typeof nodeType !== 'string') {
result.errors.push({
type: 'invalid_type',
property: 'nodeType',
message: `Invalid nodeType: expected string, got ${typeof nodeType}`,
fix: 'Provide a valid node type string (e.g., "nodes-base.webhook")'
});
return;
}
this.validateResourceAndOperation(nodeType, config, result);
this.validateSpecialTypeStructures(config, properties, result);
this.validateFixedCollectionStructures(nodeType, config, result);
const context = {
config,
errors: result.errors,
warnings: result.warnings,
suggestions: result.suggestions,
autofix: result.autofix || {}
};
const normalizedNodeType = nodeType.replace('n8n-nodes-base.', 'nodes-base.');
switch (normalizedNodeType) {
case 'nodes-base.slack':
node_specific_validators_1.NodeSpecificValidators.validateSlack(context);
this.enhanceSlackValidation(config, result);
break;
case 'nodes-base.googleSheets':
node_specific_validators_1.NodeSpecificValidators.validateGoogleSheets(context);
this.enhanceGoogleSheetsValidation(config, result);
break;
case 'nodes-base.httpRequest':
this.enhanceHttpRequestValidation(config, result);
break;
case 'nodes-base.code':
node_specific_validators_1.NodeSpecificValidators.validateCode(context);
break;
case 'nodes-base.openAi':
node_specific_validators_1.NodeSpecificValidators.validateOpenAI(context);
break;
case 'nodes-base.mongoDb':
node_specific_validators_1.NodeSpecificValidators.validateMongoDB(context);
break;
case 'nodes-base.webhook':
node_specific_validators_1.NodeSpecificValidators.validateWebhook(context);
break;
case 'nodes-base.postgres':
node_specific_validators_1.NodeSpecificValidators.validatePostgres(context);
break;
case 'nodes-base.mysql':
node_specific_validators_1.NodeSpecificValidators.validateMySQL(context);
break;
case 'nodes-langchain.agent':
node_specific_validators_1.NodeSpecificValidators.validateAIAgent(context);
break;
case 'nodes-base.set':
node_specific_validators_1.NodeSpecificValidators.validateSet(context);
break;
case 'nodes-base.switch':
this.validateSwitchNodeStructure(config, result);
break;
case 'nodes-base.if':
this.validateIfNodeStructure(config, result);
break;
case 'nodes-base.filter':
this.validateFilterNodeStructure(config, result);
break;
}
if (Object.keys(context.autofix).length > 0) {
result.autofix = context.autofix;
}
}
static enhanceSlackValidation(config, result) {
const { resource, operation } = result.operation || {};
if (resource === 'message' && operation === 'send') {
if (!config.channel && !config.channelId) {
const channelError = result.errors.find(e => e.property === 'channel' || e.property === 'channelId');
if (channelError) {
channelError.message = 'To send a Slack message, specify either a channel name (e.g., "#general") or channel ID';
channelError.fix = 'Add channel: "#general" or use a channel ID like "C1234567890"';
}
}
}
}
static enhanceGoogleSheetsValidation(config, result) {
const { operation } = result.operation || {};
if (operation === 'append') {
if (config.range && !config.range.includes('!')) {
result.warnings.push({
type: 'inefficient',
property: 'range',
message: 'Range should include sheet name (e.g., "Sheet1!A:B")',
suggestion: 'Format: "SheetName!A1:B10" or "SheetName!A:B" for entire columns'
});
}
}
}
static enhanceHttpRequestValidation(config, result) {
const url = String(config.url || '');
const options = config.options || {};
if (!result.suggestions.some(s => typeof s === 'string' && s.includes('alwaysOutputData'))) {
result.suggestions.push('Consider adding alwaysOutputData: true at node level (not in parameters) for better error handling. ' +
'This ensures the node produces output even when HTTP requests fail, allowing downstream error handling.');
}
const lowerUrl = url.toLowerCase();
const isApiEndpoint = /^https?:\/\/api\./i.test(url) ||
/\/api[\/\?]|\/api$/i.test(url) ||
/\/rest[\/\?]|\/rest$/i.test(url) ||
lowerUrl.includes('supabase.co') ||
lowerUrl.includes('firebase') ||
lowerUrl.includes('googleapis.com') ||
/\.com\/v\d+/i.test(url);
if (isApiEndpoint && !options.response?.response?.responseFormat) {
result.suggestions.push('API endpoints should explicitly set options.response.response.responseFormat to "json" or "text" ' +
'to prevent confusion about response parsing. Example: ' +
'{ "options": { "response": { "response": { "responseFormat": "json" } } } }');
}
if (url && url.startsWith('=')) {
const expressionContent = url.slice(1);
const lowerExpression = expressionContent.toLowerCase();
if (expressionContent.startsWith('www.') ||
(expressionContent.includes('{{') && !lowerExpression.includes('http'))) {
result.warnings.push({
type: 'invalid_value',
property: 'url',
message: 'URL expression appears to be missing http:// or https:// protocol',
suggestion: 'Include protocol in your expression. Example: ={{ "https://" + $json.domain + ".com" }}'
});
}
}
}
static generateNextSteps(result) {
const steps = [];
const requiredErrors = result.errors.filter(e => e.type === 'missing_required');
const typeErrors = result.errors.filter(e => e.type === 'invalid_type');
const valueErrors = result.errors.filter(e => e.type === 'invalid_value');
if (requiredErrors.length > 0) {
steps.push(`Add required fields: ${requiredErrors.map(e => e.property).join(', ')}`);
}
if (typeErrors.length > 0) {
steps.push(`Fix type mismatches: ${typeErrors.map(e => `${e.property} should be ${e.fix}`).join(', ')}`);
}
if (valueErrors.length > 0) {
steps.push(`Correct invalid values: ${valueErrors.map(e => e.property).join(', ')}`);
}
if (result.warnings.length > 0 && result.errors.length === 0) {
steps.push('Consider addressing warnings for better reliability');
}
if (result.errors.length > 0) {
steps.push('Fix the errors above following the provided suggestions');
}
return steps;
}
static deduplicateErrors(errors) {
const seen = new Map();
for (const error of errors) {
const key = `${error.property}-${error.type}`;
const existing = seen.get(key);
if (!existing) {
seen.set(key, error);
}
else {
const existingLength = (existing.message?.length || 0) + (existing.fix?.length || 0);
const newLength = (error.message?.length || 0) + (error.fix?.length || 0);
if (newLength > existingLength) {
seen.set(key, error);
}
}
}
return Array.from(seen.values());
}
static shouldFilterCredentialWarning(warning) {
return warning.type === 'security' &&
warning.message !== undefined &&
warning.message.includes('Hardcoded nodeCredentialType');
}
static applyProfileFilters(result, profile) {
switch (profile) {
case 'minimal':
result.errors = result.errors.filter(e => e.type === 'missing_required');
result.warnings = result.warnings.filter(w => {
if (this.shouldFilterCredentialWarning(w)) {
return false;
}
return w.type === 'security' || w.type === 'deprecated';
});
result.suggestions = [];
break;
case 'runtime':
result.errors = result.errors.filter(e => e.type === 'missing_required' ||
e.type === 'invalid_value' ||
(e.type === 'invalid_type' && e.message.includes('undefined')));
result.warnings = result.warnings.filter(w => {
if (this.shouldFilterCredentialWarning(w)) {
return false;
}
if (w.type === 'security' || w.type === 'deprecated')
return true;
if (w.type === 'inefficient' && w.message && w.message.includes('not visible')) {
return false;
}
return false;
});
result.suggestions = [];
break;
case 'strict':
if (result.warnings.length === 0 && result.errors.length === 0) {
result.suggestions.push('Consider adding error handling with onError property and timeout configuration');
result.suggestions.push('Add authentication if connecting to external services');
}
this.enforceErrorHandlingForProfile(result, profile);
break;
case 'ai-friendly':
default:
result.warnings = result.warnings.filter(w => {
if (this.shouldFilterCredentialWarning(w)) {
return false;
}
if (w.type === 'security' || w.type === 'deprecated')
return true;
if (w.type === 'missing_common')
return true;
if (w.type === 'best_practice')
return true;
if (w.type === 'inefficient' && w.message && w.message.includes('not visible')) {
return false;
}
if (w.type === 'inefficient' && w.property?.startsWith('_')) {
return false;
}
return true;
});
this.addErrorHandlingSuggestions(result);
break;
}
}
static enforceErrorHandlingForProfile(result, profile) {
if (profile !== 'strict')
return;
const nodeType = result.operation?.resource || '';
const errorProneTypes = ['httpRequest', 'webhook', 'database', 'api', 'slack', 'email', 'openai'];
if (errorProneTypes.some(type => nodeType.toLowerCase().includes(type))) {
result.warnings.push({
type: 'best_practice',
property: 'errorHandling',
message: 'External service nodes should have error handling configured',
suggestion: 'Add onError: "continueRegularOutput" or "stopWorkflow" with retryOnFail: true for resilience'
});
}
}
static addErrorHandlingSuggestions(result) {
const hasNetworkErrors = result.errors.some(e => e.message.toLowerCase().includes('url') ||
e.message.toLowerCase().includes('endpoint') ||
e.message.toLowerCase().includes('api'));
if (hasNetworkErrors) {
result.suggestions.push('For API calls, consider adding onError: "continueRegularOutput" with retryOnFail: true and maxTries: 3');
}
const isWebhook = result.operation?.resource === 'webhook' ||
result.errors.some(e => e.message.toLowerCase().includes('webhook'));
if (isWebhook) {
result.suggestions.push('Webhooks should use onError: "continueRegularOutput" to ensure responses are always sent');
}
}
static validateFixedCollectionStructures(nodeType, config, result) {
const validationResult = fixed_collection_validator_1.FixedCollectionValidator.validate(nodeType, config);
if (!validationResult.isValid) {
for (const error of validationResult.errors) {
result.errors.push({
type: 'invalid_value',
property: error.pattern.split('.')[0],
message: error.message,
fix: error.fix
});
}
if (validationResult.autofix) {
if (typeof validationResult.autofix === 'object' && !Array.isArray(validationResult.autofix)) {
result.autofix = {
...result.autofix,
...validationResult.autofix
};
}
else {
const firstError = validationResult.errors[0];
if (firstError) {
const rootProperty = firstError.pattern.split('.')[0];
result.autofix = {
...result.autofix,
[rootProperty]: validationResult.autofix
};
}
}
}
}
}
static validateSwitchNodeStructure(config, result) {
if (!config.rules)
return;
const hasFixedCollectionError = result.errors.some(e => e.property === 'rules' && e.message.includes('propertyValues[itemName] is not iterable'));
if (hasFixedCollectionError)
return;
if (config.rules.values && Array.isArray(config.rules.values)) {
config.rules.values.forEach((rule, index) => {
if (!rule.conditions) {
result.warnings.push({
type: 'missing_common',
property: 'rules',
message: `Switch rule ${index + 1} is missing "conditions" property`,
suggestion: 'Each rule in the values array should have a "conditions" property'
});
}
if (!rule.outputKey && rule.renameOutput !== false) {
result.warnings.push({
type: 'missing_common',
property: 'rules',
message: `Switch rule ${index + 1} is missing "outputKey" property`,
suggestion: 'Add "outputKey" to specify which output to use when this rule matches'
});
}
});
}
}
static validateIfNodeStructure(config, result) {
if (!config.conditions)
return;
const hasFixedCollectionError = result.errors.some(e => e.property === 'conditions' && e.message.includes('propertyValues[itemName] is not iterable'));
if (hasFixedCollectionError)
return;
}
static validateFilterNodeStructure(config, result) {
if (!config.conditions)
return;
const hasFixedCollectionError = result.errors.some(e => e.property === 'conditions' && e.message.includes('propertyValues[itemName] is not iterable'));
if (hasFixedCollectionError)
return;
}
static validateResourceAndOperation(nodeType, config, result) {
if (!this.operationSimilarityService || !this.resourceSimilarityService || !this.nodeRepository) {
return;
}
const normalizedNodeType = node_type_normalizer_1.NodeTypeNormalizer.normalizeToFullForm(nodeType);
const configWithDefaults = { ...config };
if (configWithDefaults.operation === undefined && configWithDefaults.resource !== undefined) {
const defaultOperation = this.nodeRepository.getDefaultOperationForResource(normalizedNodeType, configWithDefaults.resource);
if (defaultOperation !== undefined) {
configWithDefaults.operation = defaultOperation;
}
}
if (config.resource !== undefined) {
result.errors = result.errors.filter(e => e.property !== 'resource');
const validResources = this.nodeRepository.getNodeResources(normalizedNodeType);
const resourceIsValid = validResources.some(r => {
const resourceValue = typeof r === 'string' ? r : r.value;
return resourceValue === config.resource;
});
if (!resourceIsValid && config.resource !== '') {
let suggestions = [];
try {
suggestions = this.resourceSimilarityService.findSimilarResources(normalizedNodeType, config.resource, 3);
}
catch (error) {
console.error('Resource similarity service error:', error);
}
let errorMessage = `Invalid resource "${config.resource}" for node ${nodeType}.`;
let fix = '';
if (suggestions.length > 0) {
const topSuggestion = suggestions[0];
errorMessage += ` Did you mean "${topSuggestion.value}"?`;
if (topSuggestion.confidence >= 0.8) {
fix = `Change resource to "${topSuggestion.value}". ${topSuggestion.reason}`;
}
else {
fix = `Valid resources: ${validResources.slice(0, 5).map(r => {
const val = typeof r === 'string' ? r : r.value;
return `"${val}"`;
}).join(', ')}${validResources.length > 5 ? '...' : ''}`;
}
}
else {
fix = `Valid resources: ${validResources.slice(0, 5).map(r => {
const val = typeof r === 'string' ? r : r.value;
return `"${val}"`;
}).join(', ')}${validResources.length > 5 ? '...' : ''}`;
}
const error = {
type: 'invalid_value',
property: 'resource',
message: errorMessage,
fix
};
if (suggestions.length > 0 && suggestions[0].confidence >= 0.5) {
error.suggestion = `Did you mean "${suggestions[0].value}"? ${suggestions[0].reason}`;
}
result.errors.push(error);
if (suggestions.length > 0) {
for (const suggestion of suggestions) {
result.suggestions.push(`Resource "${config.resource}" not found. Did you mean "${suggestion.value}"? ${suggestion.reason}`);
}
}
}
}
if (config.operation !== undefined || configWithDefaults.operation !== undefined) {
result.errors = result.errors.filter(e => e.property !== 'operation');
const operationToValidate = configWithDefaults.operation || config.operation;
const validOperations = this.nodeRepository.getNodeOperations(normalizedNodeType, config.resource);
const operationIsValid = validOperations.some(op => {
const opValue = op.operation || op.value || op;
return opValue === operationToValidate;
});
if (!operationIsValid && config.operation !== undefined && config.operation !== '') {
let suggestions = [];
try {
suggestions = this.operationSimilarityService.findSimilarOperations(normalizedNodeType, config.operation, config.resource, 3);
}
catch (error) {
console.error('Operation similarity service error:', error);
}
let errorMessage = `Invalid operation "${config.operation}" for node ${nodeType}`;
if (config.resource) {
errorMessage += ` with resource "${config.resource}"`;
}
errorMessage += '.';
let fix = '';
if (suggestions.length > 0) {
const topSuggestion = suggestions[0];
if (topSuggestion.confidence >= 0.8) {
errorMessage += ` Did you mean "${topSuggestion.value}"?`;
fix = `Change operation to "${topSuggestion.value}". ${topSuggestion.reason}`;
}
else {
errorMessage += ` Similar operations: ${suggestions.map(s => `"${s.value}"`).join(', ')}`;
fix = `Valid operations${config.resource ? ` for resource "${config.resource}"` : ''}: ${validOperations.slice(0, 5).map(op => {
const val = op.operation || op.value || op;
return `"${val}"`;
}).join(', ')}${validOperations.length > 5 ? '...' : ''}`;
}
}
else {
fix = `Valid operations${config.resource ? ` for resource "${config.resource}"` : ''}: ${validOperations.slice(0, 5).map(op => {
const val = op.operation || op.value || op;
return `"${val}"`;
}).join(', ')}${validOperations.length > 5 ? '...' : ''}`;
}
const error = {
type: 'invalid_value',
property: 'operation',
message: errorMessage,
fix
};
if (suggestions.length > 0 && suggestions[0].confidence >= 0.5) {
error.suggestion = `Did you mean "${suggestions[0].value}"? ${suggestions[0].reason}`;
}
result.errors.push(error);
if (suggestions.length > 0) {
for (const suggestion of suggestions) {
result.suggestions.push(`Operation "${config.operation}" not found. Did you mean "${suggestion.value}"? ${suggestion.reason}`);
}
}
}
}
}
static validateSpecialTypeStructures(config, properties, result) {
for (const [key, value] of Object.entries(config)) {
if (value === undefined || value === null)
continue;
const propDef = properties.find(p => p.name === key);
if (!propDef)
continue;
let structureType = null;
if (propDef.type === 'filter') {
structureType = 'filter';
}
else if (propDef.type === 'resourceMapper') {
structureType = 'resourceMapper';
}
else if (propDef.type === 'assignmentCollection') {
structureType = 'assignmentCollection';
}
else if (propDef.type === 'resourceLocator') {
structureType = 'resourceLocator';
}
if (!structureType)
continue;
const structure = type_structure_service_1.TypeStructureService.getStructure(structureType);
if (!structure) {
console.warn(`No structure definition found for type: ${structureType}`);
continue;
}
const validationResult = type_structure_service_1.TypeStructureService.validateTypeCompatibility(value, structureType);
if (!validationResult.valid) {
for (const error of validationResult.errors) {
result.errors.push({
type: 'invalid_configuration',
property: key,
message: error,
fix: `Ensure ${key} follows the expected structure for ${structureType} type. Example: ${JSON.stringify(structure.example)}`
});
}
}
for (const warning of validationResult.warnings) {
result.warnings.push({
type: 'best_practice',
property: key,
message: warning
});
}
if (typeof value === 'object' && value !== null) {
this.validateComplexTypeStructure(key, value, structureType, structure, result);
}
if (structureType === 'filter' && value.conditions) {
this.validateFilterOperations(value.conditions, key, result);
}
}
}
static validateComplexTypeStructure(propertyName, value, type, structure, result) {
switch (type) {
case 'filter':
if (!value.combinator) {
result.errors.push({
type: 'invalid_configuration',
property: `${propertyName}.combinator`,
message: 'Filter must have a combinator field',
fix: 'Add combinator: "and" or combinator: "or" to the filter configuration'
});
}
else if (value.combinator !== 'and' && value.combinator !== 'or') {
result.errors.push({
type: 'invalid_configuration',
property: `${propertyName}.combinator`,
message: `Invalid combinator value: ${value.combinator}. Must be "and" or "or"`,
fix: 'Set combinator to either "and" or "or"'
});
}
if (!value.conditions) {
result.errors.push({
type: 'invalid_configuration',
property: `${propertyName}.conditions`,
message: 'Filter must have a conditions field',
fix: 'Add conditions array to the filter configuration'
});
}
else if (!Array.isArray(value.conditions)) {
result.errors.push({
type: 'invalid_configuration',
property: `${propertyName}.conditions`,
message: 'Filter conditions must be an array',
fix: 'Ensure conditions is an array of condition objects'
});
}
break;
case 'resourceLocator':
if (!value.mode) {
result.errors.push({
type: 'invalid_configuration',
property: `${propertyName}.mode`,
message: 'ResourceLocator must have a mode field',
fix: 'Add mode: "id", mode: "url", or mode: "list" to the resourceLocator configuration'
});
}
else if (!['id', 'url', 'list', 'name'].includes(value.mode)) {
result.errors.push({
type: 'invalid_configuration',
property: `${propertyName}.mode`,
message: `Invalid mode value: ${value.mode}. Must be "id", "url", "list", or "name"`,
fix: 'Set mode to one of: "id", "url", "list", "name"'
});
}
if (!value.hasOwnProperty('value')) {
result.errors.push({
type: 'invalid_configuration',
property: `${propertyName}.value`,
message: 'ResourceLocator must have a value field',
fix: 'Add value field to the resourceLocator configuration'
});
}
break;
case 'assignmentCollection':
if (!value.assignments) {
result.errors.push({
type: 'invalid_configuration',
property: `${propertyName}.assignments`,
message: 'AssignmentCollection must have an assignments field',
fix: 'Add assignments array to the assignmentCollection configuration'
});
}
else if (!Array.isArray(value.assignments)) {
result.errors.push({
type: 'invalid_configuration',
property: `${propertyName}.assignments`,
message: 'AssignmentCollection assignments must be an array',
fix: 'Ensure assignments is an array of assignment objects'
});
}
break;
case 'resourceMapper':
if (!value.mappingMode) {
result.errors.push({
type: 'invalid_configuration',
property: `${propertyName}.mappingMode`,
message: 'ResourceMapper must have a mappingMode field',
fix: 'Add mappingMode: "defineBelow" or mappingMode: "autoMapInputData"'
});
}
else if (!['defineBelow', 'autoMapInputData'].includes(value.mappingMode)) {
result.errors.push({
type: 'invalid_configuration',
property: `${propertyName}.mappingMode`,
message: `Invalid mappingMode: ${value.mappingMode}. Must be "defineBelow" or "autoMapInputData"`,
fix: 'Set mappingMode to either "defineBelow" or "autoMapInputData"'
});
}
break;
}
}
static validateFilterOperations(conditions, propertyName, result) {
if (!Array.isArray(conditions))
return;
const VALID_OPERATIONS_BY_TYPE = {
string: [
'empty', 'notEmpty', 'equals', 'notEquals',
'contains', 'notContains', 'startsWith', 'notStartsWith',
'endsWith', 'notEndsWith', 'regex', 'notRegex',
'exists', 'notExists', 'isNotEmpty'
],
number: [
'empty', 'notEmpty', 'equals', 'notEquals', 'gt', 'lt', 'gte', 'lte',
'exists', 'notExists', 'isNotEmpty'
],
dateTime: [
'empty', 'notEmpty', 'equals', 'notEquals', 'after', 'before', 'afterOrEquals', 'beforeOrEquals',
'exists', 'notExists', 'isNotEmpty'
],
boolean: [
'empty', 'notEmpty', 'true', 'false', 'equals', 'notEquals',
'exists', 'notExists', 'isNotEmpty'
],
array: [
'contains', 'notContains', 'lengthEquals', 'lengthNotEquals',
'lengthGt', 'lengthLt', 'lengthGte', 'lengthLte', 'empty', 'notEmpty',
'exists', 'notExists', 'isNotEmpty'
],
object: [
'empty', 'notEmpty',
'exists', 'notExists', 'isNotEmpty'
],
any: ['exists', 'notExists', 'isNotEmpty']
};
for (let i = 0; i < conditions.length; i++) {
const condition = conditions[i];
if (!condition.operator || typeof condition.operator !== 'object')
continue;
const { type, operation } = condition.operator;
if (!type || !operation)
continue;
const validOperations = VALID_OPERATIONS_BY_TYPE[type];
if (!validOperations) {
result.warnings.push({
type: 'best_practice',
property: `${propertyName}.conditions[${i}].operator.type`,
message: `Unknown operator type: ${type}`
});
continue;
}
if (!validOperations.includes(operation)) {
result.errors.push({
type: 'invalid_value',
property: `${propertyName}.conditions[${i}].operator.operation`,
message: `Operation '${operation}' is not valid for type '${type}'`,
fix: `Use one of the valid operations for ${type}: ${validOperations.join(', ')}`
});
}
}
}
}
exports.EnhancedConfigValidator = EnhancedConfigValidator;
EnhancedConfigValidator.operationSimilarityService = null;
EnhancedConfigValidator.resourceSimilarityService = null;
EnhancedConfigValidator.nodeRepository = null;
//# sourceMappingURL=enhanced-config-validator.js.map

File diff suppressed because one or more lines are too long

14
dist/services/example-generator.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
export interface NodeExamples {
minimal: Record<string, any>;
common?: Record<string, any>;
advanced?: Record<string, any>;
}
export declare class ExampleGenerator {
private static NODE_EXAMPLES;
static getExamples(nodeType: string, essentials?: any): NodeExamples;
private static generateBasicExamples;
private static getDefaultValue;
private static getStringDefault;
static getTaskExample(nodeType: string, task: string): Record<string, any> | undefined;
}
//# sourceMappingURL=example-generator.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"example-generator.d.ts","sourceRoot":"","sources":["../../src/services/example-generator.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAChC;AAED,qBAAa,gBAAgB;IAK3B,OAAO,CAAC,MAAM,CAAC,aAAa,CA87B1B;IAKF,MAAM,CAAC,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,GAAG,GAAG,YAAY;IAcpE,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAsBpC,OAAO,CAAC,MAAM,CAAC,eAAe;IAsC9B,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAiD/B,MAAM,CAAC,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS;CAiBvF"}

970
dist/services/example-generator.js vendored Normal file
View File

@@ -0,0 +1,970 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExampleGenerator = void 0;
class ExampleGenerator {
static getExamples(nodeType, essentials) {
const examples = this.NODE_EXAMPLES[nodeType];
if (examples) {
return examples;
}
return this.generateBasicExamples(nodeType, essentials);
}
static generateBasicExamples(nodeType, essentials) {
const minimal = {};
if (essentials?.required) {
for (const prop of essentials.required) {
minimal[prop.name] = this.getDefaultValue(prop);
}
}
if (Object.keys(minimal).length === 0 && essentials?.common?.length > 0) {
const firstCommon = essentials.common[0];
minimal[firstCommon.name] = this.getDefaultValue(firstCommon);
}
return { minimal };
}
static getDefaultValue(prop) {
if (prop.default !== undefined) {
return prop.default;
}
switch (prop.type) {
case 'string':
return this.getStringDefault(prop);
case 'number':
return prop.name.includes('port') ? 80 :
prop.name.includes('timeout') ? 30000 :
prop.name.includes('limit') ? 10 : 0;
case 'boolean':
return false;
case 'options':
case 'multiOptions':
return prop.options?.[0]?.value || '';
case 'json':
return '{\n "key": "value"\n}';
case 'collection':
case 'fixedCollection':
return {};
default:
return '';
}
}
static getStringDefault(prop) {
const name = prop.name.toLowerCase();
if (name.includes('url') || name === 'endpoint') {
return 'https://api.example.com';
}
if (name.includes('email')) {
return name.includes('from') ? 'sender@example.com' : 'recipient@example.com';
}
if (name.includes('path')) {
return name.includes('webhook') ? 'my-webhook' : '/path/to/file';
}
if (name === 'name' || name.includes('username')) {
return 'John Doe';
}
if (name.includes('key')) {
return 'myKey';
}
if (name === 'query' || name.includes('sql')) {
return 'SELECT * FROM table_name LIMIT 10';
}
if (name === 'collection' || name === 'table') {
return 'users';
}
if (prop.placeholder) {
return prop.placeholder;
}
return '';
}
static getTaskExample(nodeType, task) {
const examples = this.NODE_EXAMPLES[nodeType];
if (!examples)
return undefined;
const taskMap = {
'basic': 'minimal',
'simple': 'minimal',
'typical': 'common',
'standard': 'common',
'complex': 'advanced',
'full': 'advanced'
};
const exampleType = taskMap[task] || 'common';
return examples[exampleType] || examples.minimal;
}
}
exports.ExampleGenerator = ExampleGenerator;
ExampleGenerator.NODE_EXAMPLES = {
'nodes-base.httpRequest': {
minimal: {
url: 'https://api.example.com/data'
},
common: {
method: 'POST',
url: 'https://api.example.com/users',
sendBody: true,
contentType: 'json',
specifyBody: 'json',
jsonBody: '{\n "name": "John Doe",\n "email": "john@example.com"\n}'
},
advanced: {
method: 'POST',
url: 'https://api.example.com/protected/resource',
authentication: 'genericCredentialType',
genericAuthType: 'headerAuth',
sendHeaders: true,
headerParameters: {
parameters: [
{
name: 'X-API-Version',
value: 'v2'
}
]
},
sendBody: true,
contentType: 'json',
specifyBody: 'json',
jsonBody: '{\n "action": "update",\n "data": {}\n}',
onError: 'continueRegularOutput',
retryOnFail: true,
maxTries: 3,
waitBetweenTries: 1000,
alwaysOutputData: true
}
},
'nodes-base.webhook': {
minimal: {
path: 'my-webhook',
httpMethod: 'POST'
},
common: {
path: 'webhook-endpoint',
httpMethod: 'POST',
responseMode: 'lastNode',
responseData: 'allEntries',
responseCode: 200,
onError: 'continueRegularOutput',
alwaysOutputData: true
}
},
'nodes-base.code.webhookProcessing': {
minimal: {
language: 'javaScript',
jsCode: `// ⚠️ CRITICAL: Webhook data is nested under 'body' property!
// This Code node should be connected after a Webhook node
// ❌ WRONG - This will be undefined:
// const command = items[0].json.testCommand;
// ✅ CORRECT - Access webhook data through body:
const webhookData = items[0].json.body;
const headers = items[0].json.headers;
const query = items[0].json.query;
// Process webhook payload
return [{
json: {
// Extract data from webhook body
command: webhookData.testCommand,
userId: webhookData.userId,
data: webhookData.data,
// Add metadata
timestamp: DateTime.now().toISO(),
requestId: headers['x-request-id'] || crypto.randomUUID(),
source: query.source || 'webhook',
// Original webhook info
httpMethod: items[0].json.httpMethod,
webhookPath: items[0].json.webhookPath
}
}];`
}
},
'nodes-base.code': {
minimal: {
language: 'javaScript',
jsCode: 'return [{json: {result: "success"}}];'
},
common: {
language: 'javaScript',
jsCode: `// Process each item and add timestamp
return items.map(item => ({
json: {
...item.json,
processed: true,
timestamp: DateTime.now().toISO()
}
}));`,
onError: 'continueRegularOutput'
},
advanced: {
language: 'javaScript',
jsCode: `// Advanced data processing with proper helper checks
const crypto = require('crypto');
const results = [];
for (const item of items) {
try {
// Validate required fields
if (!item.json.email || !item.json.name) {
throw new Error('Missing required fields: email or name');
}
// Generate secure API key
const apiKey = crypto.randomBytes(16).toString('hex');
// Check if $helpers is available before using
let response;
if (typeof $helpers !== 'undefined' && $helpers.httpRequest) {
response = await $helpers.httpRequest({
method: 'POST',
url: 'https://api.example.com/process',
body: {
email: item.json.email,
name: item.json.name,
apiKey
},
headers: {
'Content-Type': 'application/json'
}
});
} else {
// Fallback if $helpers not available
response = { message: 'HTTP requests not available in this n8n version' };
}
// Add to results with response data
results.push({
json: {
...item.json,
apiResponse: response,
processedAt: DateTime.now().toISO(),
status: 'success'
}
});
} catch (error) {
// Include failed items with error info
results.push({
json: {
...item.json,
error: error.message,
status: 'failed',
processedAt: DateTime.now().toISO()
}
});
}
}
return results;`,
onError: 'continueRegularOutput',
retryOnFail: true,
maxTries: 2
}
},
'nodes-base.code.dataTransform': {
minimal: {
language: 'javaScript',
jsCode: `// Transform CSV-like data to JSON
return items.map(item => {
const lines = item.json.data.split('\\n');
const headers = lines[0].split(',');
const rows = lines.slice(1).map(line => {
const values = line.split(',');
return headers.reduce((obj, header, i) => {
obj[header.trim()] = values[i]?.trim() || '';
return obj;
}, {});
});
return {json: {rows, count: rows.length}};
});`
}
},
'nodes-base.code.aggregation': {
minimal: {
language: 'javaScript',
jsCode: `// Aggregate data from all items
const totals = items.reduce((acc, item) => {
acc.count++;
acc.sum += item.json.amount || 0;
acc.categories[item.json.category] = (acc.categories[item.json.category] || 0) + 1;
return acc;
}, {count: 0, sum: 0, categories: {}});
return [{
json: {
totalItems: totals.count,
totalAmount: totals.sum,
averageAmount: totals.sum / totals.count,
categoryCounts: totals.categories,
processedAt: DateTime.now().toISO()
}
}];`
}
},
'nodes-base.code.filtering': {
minimal: {
language: 'javaScript',
jsCode: `// Filter items based on conditions
return items
.filter(item => {
const amount = item.json.amount || 0;
const status = item.json.status || '';
return amount > 100 && status === 'active';
})
.map(item => ({json: item.json}));`
}
},
'nodes-base.code.jmespathFiltering': {
minimal: {
language: 'javaScript',
jsCode: `// JMESPath filtering - IMPORTANT: Use backticks for numeric literals!
const allItems = items.map(item => item.json);
// ✅ CORRECT - Filter with numeric literals using backticks
const expensiveItems = $jmespath(allItems, '[?price >= \`100\`]');
const lowStock = $jmespath(allItems, '[?inventory < \`10\`]');
const highPriority = $jmespath(allItems, '[?priority == \`1\`]');
// Combine multiple conditions
const urgentExpensive = $jmespath(allItems, '[?price >= \`100\` && priority == \`1\`]');
// String comparisons don't need backticks
const activeItems = $jmespath(allItems, '[?status == "active"]');
// Return filtered results
return expensiveItems.map(item => ({json: item}));`
}
},
'nodes-base.code.pythonExample': {
minimal: {
language: 'python',
pythonCode: `# Python data processing - use underscore prefix for built-in variables
import json
from datetime import datetime
import re
results = []
# Use _input.all() to get items in Python
for item in _input.all():
# Convert JsProxy to Python dict to avoid issues with null values
item_data = item.json.to_py()
# Clean email addresses
email = item_data.get('email', '')
if email and re.match(r'^[\\w\\.-]+@[\\w\\.-]+\\.\\w+$', email):
cleaned_data = {
'email': email.lower(),
'name': item_data.get('name', '').title(),
'validated': True,
'timestamp': datetime.now().isoformat()
}
else:
# Spread operator doesn't work with JsProxy, use dict()
cleaned_data = dict(item_data)
cleaned_data['validated'] = False
cleaned_data['error'] = 'Invalid email format'
results.append({'json': cleaned_data})
return results`
}
},
'nodes-base.code.aiTool': {
minimal: {
language: 'javaScript',
mode: 'runOnceForEachItem',
jsCode: `// Code node as AI tool - calculate discount
const quantity = $json.quantity || 1;
const price = $json.price || 0;
let discountRate = 0;
if (quantity >= 100) discountRate = 0.20;
else if (quantity >= 50) discountRate = 0.15;
else if (quantity >= 20) discountRate = 0.10;
else if (quantity >= 10) discountRate = 0.05;
const subtotal = price * quantity;
const discount = subtotal * discountRate;
const total = subtotal - discount;
return [{
json: {
quantity,
price,
subtotal,
discountRate: discountRate * 100,
discountAmount: discount,
total,
savings: discount
}
}];`
}
},
'nodes-base.code.crypto': {
minimal: {
language: 'javaScript',
jsCode: `// Using crypto in Code nodes - it IS available!
const crypto = require('crypto');
// Generate secure tokens
const token = crypto.randomBytes(32).toString('hex');
const uuid = crypto.randomUUID();
// Create hashes
const hash = crypto.createHash('sha256')
.update(items[0].json.data || 'test')
.digest('hex');
return [{
json: {
token,
uuid,
hash,
timestamp: DateTime.now().toISO()
}
}];`
}
},
'nodes-base.code.staticData': {
minimal: {
language: 'javaScript',
jsCode: `// Using workflow static data correctly
// IMPORTANT: $getWorkflowStaticData is a standalone function!
const staticData = $getWorkflowStaticData('global');
// Initialize counter if not exists
if (!staticData.processCount) {
staticData.processCount = 0;
staticData.firstRun = DateTime.now().toISO();
}
// Update counter
staticData.processCount++;
staticData.lastRun = DateTime.now().toISO();
// Process items
const results = items.map(item => ({
json: {
...item.json,
runNumber: staticData.processCount,
processed: true
}
}));
return results;`
}
},
'nodes-base.set': {
minimal: {
mode: 'manual',
assignments: {
assignments: [
{
id: '1',
name: 'status',
value: 'active',
type: 'string'
}
]
}
},
common: {
mode: 'manual',
includeOtherFields: true,
assignments: {
assignments: [
{
id: '1',
name: 'status',
value: 'processed',
type: 'string'
},
{
id: '2',
name: 'processedAt',
value: '={{ $now.toISO() }}',
type: 'string'
},
{
id: '3',
name: 'itemCount',
value: '={{ $items().length }}',
type: 'number'
}
]
}
}
},
'nodes-base.if': {
minimal: {
conditions: {
conditions: [
{
id: '1',
leftValue: '={{ $json.status }}',
rightValue: 'active',
operator: {
type: 'string',
operation: 'equals'
}
}
]
}
},
common: {
conditions: {
conditions: [
{
id: '1',
leftValue: '={{ $json.status }}',
rightValue: 'active',
operator: {
type: 'string',
operation: 'equals'
}
},
{
id: '2',
leftValue: '={{ $json.count }}',
rightValue: 10,
operator: {
type: 'number',
operation: 'gt'
}
}
]
},
combineOperation: 'all'
}
},
'nodes-base.postgres': {
minimal: {
operation: 'executeQuery',
query: 'SELECT * FROM users LIMIT 10'
},
common: {
operation: 'insert',
table: 'users',
columns: 'name,email,created_at',
additionalFields: {}
},
advanced: {
operation: 'executeQuery',
query: `INSERT INTO users (name, email, status)
VALUES ($1, $2, $3)
ON CONFLICT (email)
DO UPDATE SET
name = EXCLUDED.name,
updated_at = NOW()
RETURNING *;`,
additionalFields: {
queryParams: '={{ $json.name }},{{ $json.email }},active'
},
retryOnFail: true,
maxTries: 3,
waitBetweenTries: 2000,
onError: 'continueErrorOutput'
}
},
'nodes-base.openAi': {
minimal: {
resource: 'chat',
operation: 'message',
modelId: 'gpt-3.5-turbo',
messages: {
values: [
{
role: 'user',
content: 'Hello, how can you help me?'
}
]
}
},
common: {
resource: 'chat',
operation: 'message',
modelId: 'gpt-4',
messages: {
values: [
{
role: 'system',
content: 'You are a helpful assistant that summarizes text concisely.'
},
{
role: 'user',
content: '={{ $json.text }}'
}
]
},
options: {
maxTokens: 150,
temperature: 0.7
},
retryOnFail: true,
maxTries: 3,
waitBetweenTries: 5000,
onError: 'continueRegularOutput',
alwaysOutputData: true
}
},
'nodes-base.googleSheets': {
minimal: {
operation: 'read',
documentId: {
__rl: true,
value: 'https://docs.google.com/spreadsheets/d/your-sheet-id',
mode: 'url'
},
sheetName: 'Sheet1'
},
common: {
operation: 'append',
documentId: {
__rl: true,
value: 'your-sheet-id',
mode: 'id'
},
sheetName: 'Sheet1',
dataStartRow: 2,
columns: {
mappingMode: 'defineBelow',
value: {
'Name': '={{ $json.name }}',
'Email': '={{ $json.email }}',
'Date': '={{ $now.toISO() }}'
}
}
}
},
'nodes-base.slack': {
minimal: {
resource: 'message',
operation: 'post',
channel: '#general',
text: 'Hello from n8n!'
},
common: {
resource: 'message',
operation: 'post',
channel: '#notifications',
text: 'New order received!',
attachments: [
{
color: '#36a64f',
title: 'Order #{{ $json.orderId }}',
fields: {
item: [
{
title: 'Customer',
value: '{{ $json.customerName }}',
short: true
},
{
title: 'Amount',
value: '${{ $json.amount }}',
short: true
}
]
}
}
],
retryOnFail: true,
maxTries: 2,
waitBetweenTries: 3000,
onError: 'continueRegularOutput'
}
},
'nodes-base.emailSend': {
minimal: {
fromEmail: 'sender@example.com',
toEmail: 'recipient@example.com',
subject: 'Test Email',
text: 'This is a test email from n8n.'
},
common: {
fromEmail: 'notifications@company.com',
toEmail: '={{ $json.email }}',
subject: 'Welcome to our service, {{ $json.name }}!',
html: `<h1>Welcome!</h1>
<p>Hi {{ $json.name }},</p>
<p>Thank you for signing up. We're excited to have you on board!</p>
<p>Best regards,<br>The Team</p>`,
options: {
ccEmail: 'admin@company.com'
},
retryOnFail: true,
maxTries: 3,
waitBetweenTries: 2000,
onError: 'continueRegularOutput'
}
},
'nodes-base.merge': {
minimal: {
mode: 'append'
},
common: {
mode: 'mergeByKey',
propertyName1: 'id',
propertyName2: 'userId'
}
},
'nodes-base.function': {
minimal: {
functionCode: 'return items;'
},
common: {
functionCode: `// Add a timestamp to each item
const processedItems = items.map(item => {
return {
...item,
json: {
...item.json,
processedAt: new Date().toISOString()
}
};
});
return processedItems;`
}
},
'nodes-base.splitInBatches': {
minimal: {
batchSize: 10
},
common: {
batchSize: 100,
options: {
reset: false
}
}
},
'nodes-base.redis': {
minimal: {
operation: 'set',
key: 'myKey',
value: 'myValue'
},
common: {
operation: 'set',
key: 'user:{{ $json.userId }}',
value: '={{ JSON.stringify($json) }}',
expire: true,
ttl: 3600
}
},
'nodes-base.mongoDb': {
minimal: {
operation: 'find',
collection: 'users'
},
common: {
operation: 'findOneAndUpdate',
collection: 'users',
query: '{ "email": "{{ $json.email }}" }',
update: '{ "$set": { "lastLogin": "{{ $now.toISO() }}" } }',
options: {
upsert: true,
returnNewDocument: true
},
retryOnFail: true,
maxTries: 3,
waitBetweenTries: 1000,
onError: 'continueErrorOutput'
}
},
'nodes-base.mySql': {
minimal: {
operation: 'executeQuery',
query: 'SELECT * FROM products WHERE active = 1'
},
common: {
operation: 'insert',
table: 'orders',
columns: 'customer_id,product_id,quantity,order_date',
options: {
queryBatching: 'independently'
},
retryOnFail: true,
maxTries: 3,
waitBetweenTries: 2000,
onError: 'stopWorkflow'
}
},
'nodes-base.ftp': {
minimal: {
operation: 'download',
path: '/files/data.csv'
},
common: {
operation: 'upload',
path: '/uploads/',
fileName: 'report_{{ $now.format("yyyy-MM-dd") }}.csv',
binaryData: true,
binaryPropertyName: 'data'
}
},
'nodes-base.ssh': {
minimal: {
resource: 'command',
operation: 'execute',
command: 'ls -la'
},
common: {
resource: 'command',
operation: 'execute',
command: 'cd /var/logs && tail -n 100 app.log | grep ERROR',
cwd: '/home/user'
}
},
'nodes-base.executeCommand': {
minimal: {
command: 'echo "Hello from n8n"'
},
common: {
command: 'node process-data.js --input "{{ $json.filename }}"',
cwd: '/app/scripts'
}
},
'nodes-base.github': {
minimal: {
resource: 'issue',
operation: 'get',
owner: 'n8n-io',
repository: 'n8n',
issueNumber: 123
},
common: {
resource: 'issue',
operation: 'create',
owner: '={{ $json.organization }}',
repository: '={{ $json.repo }}',
title: 'Bug: {{ $json.title }}',
body: `## Description
{{ $json.description }}
## Steps to Reproduce
{{ $json.steps }}
## Expected Behavior
{{ $json.expected }}`,
assignees: ['maintainer'],
labels: ['bug', 'needs-triage']
}
},
'error-handling.modern-patterns': {
minimal: {
onError: 'continueRegularOutput'
},
common: {
onError: 'continueErrorOutput',
alwaysOutputData: true
},
advanced: {
onError: 'stopWorkflow',
retryOnFail: true,
maxTries: 3,
waitBetweenTries: 2000
}
},
'error-handling.api-with-retry': {
minimal: {
url: 'https://api.example.com/data',
retryOnFail: true,
maxTries: 3,
waitBetweenTries: 1000
},
common: {
method: 'GET',
url: 'https://api.example.com/users/{{ $json.userId }}',
retryOnFail: true,
maxTries: 5,
waitBetweenTries: 2000,
alwaysOutputData: true,
sendHeaders: true,
headerParameters: {
parameters: [
{
name: 'X-Request-ID',
value: '={{ $workflow.id }}-{{ $execution.id }}'
}
]
}
},
advanced: {
method: 'POST',
url: 'https://api.example.com/critical-operation',
sendBody: true,
contentType: 'json',
specifyBody: 'json',
jsonBody: '{{ JSON.stringify($json) }}',
retryOnFail: true,
maxTries: 5,
waitBetweenTries: 1000,
alwaysOutputData: true,
onError: 'stopWorkflow'
}
},
'error-handling.fault-tolerant': {
minimal: {
onError: 'continueRegularOutput'
},
common: {
onError: 'continueRegularOutput',
alwaysOutputData: true
},
advanced: {
onError: 'continueRegularOutput',
retryOnFail: true,
maxTries: 2,
waitBetweenTries: 500,
alwaysOutputData: true
}
},
'error-handling.database-patterns': {
minimal: {
onError: 'continueRegularOutput',
alwaysOutputData: true
},
common: {
retryOnFail: true,
maxTries: 3,
waitBetweenTries: 2000,
onError: 'stopWorkflow'
},
advanced: {
onError: 'continueErrorOutput',
retryOnFail: false,
alwaysOutputData: true
}
},
'error-handling.webhook-patterns': {
minimal: {
onError: 'continueRegularOutput',
alwaysOutputData: true
},
common: {
onError: 'continueErrorOutput',
alwaysOutputData: true,
responseCode: 200,
responseData: 'allEntries'
}
},
'error-handling.ai-patterns': {
minimal: {
retryOnFail: true,
maxTries: 3,
waitBetweenTries: 5000,
onError: 'continueRegularOutput'
},
common: {
retryOnFail: true,
maxTries: 5,
waitBetweenTries: 2000,
onError: 'continueRegularOutput',
alwaysOutputData: true
}
}
};
//# sourceMappingURL=example-generator.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,8 @@
import { Execution, ExecutionPreview, ExecutionRecommendation, ExecutionFilterOptions, FilteredExecutionResponse } from '../types/n8n-api';
export declare function generatePreview(execution: Execution): {
preview: ExecutionPreview;
recommendation: ExecutionRecommendation;
};
export declare function filterExecutionData(execution: Execution, options: ExecutionFilterOptions): FilteredExecutionResponse;
export declare function processExecution(execution: Execution, options?: ExecutionFilterOptions): FilteredExecutionResponse | Execution;
//# sourceMappingURL=execution-processor.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"execution-processor.d.ts","sourceRoot":"","sources":["../../src/services/execution-processor.ts"],"names":[],"mappings":"AAaA,OAAO,EACL,SAAS,EAET,gBAAgB,EAEhB,uBAAuB,EACvB,sBAAsB,EACtB,yBAAyB,EAG1B,MAAM,kBAAkB,CAAC;AA+G1B,wBAAgB,eAAe,CAAC,SAAS,EAAE,SAAS,GAAG;IACrD,OAAO,EAAE,gBAAgB,CAAC;IAC1B,cAAc,EAAE,uBAAuB,CAAC;CACzC,CA2EA;AAoID,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,sBAAsB,GAC9B,yBAAyB,CA2J3B;AAMD,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,SAAS,EACpB,OAAO,GAAE,sBAA2B,GACnC,yBAAyB,GAAG,SAAS,CAOvC"}

359
dist/services/execution-processor.js vendored Normal file
View File

@@ -0,0 +1,359 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.generatePreview = generatePreview;
exports.filterExecutionData = filterExecutionData;
exports.processExecution = processExecution;
const logger_1 = require("../utils/logger");
const THRESHOLDS = {
CHAR_SIZE_BYTES: 2,
OVERHEAD_PER_OBJECT: 50,
MAX_RECOMMENDED_SIZE_KB: 100,
SMALL_DATASET_ITEMS: 20,
MODERATE_DATASET_ITEMS: 50,
MODERATE_DATASET_SIZE_KB: 200,
MAX_DEPTH: 3,
MAX_ITEMS_LIMIT: 1000,
};
function extractErrorMessage(error) {
if (typeof error === 'string') {
return error;
}
if (error && typeof error === 'object') {
if ('message' in error && typeof error.message === 'string') {
return error.message;
}
if ('error' in error && typeof error.error === 'string') {
return error.error;
}
}
return 'Unknown error';
}
function extractStructure(data, maxDepth = THRESHOLDS.MAX_DEPTH, currentDepth = 0) {
if (currentDepth >= maxDepth) {
return typeof data;
}
if (data === null || data === undefined) {
return 'null';
}
if (Array.isArray(data)) {
if (data.length === 0) {
return [];
}
return [extractStructure(data[0], maxDepth, currentDepth + 1)];
}
if (typeof data === 'object') {
const structure = {};
for (const key in data) {
if (Object.prototype.hasOwnProperty.call(data, key)) {
structure[key] = extractStructure(data[key], maxDepth, currentDepth + 1);
}
}
return structure;
}
return typeof data;
}
function estimateDataSize(data) {
try {
const jsonString = JSON.stringify(data);
const sizeBytes = jsonString.length * THRESHOLDS.CHAR_SIZE_BYTES;
return Math.ceil(sizeBytes / 1024);
}
catch (error) {
logger_1.logger.warn('Failed to estimate data size', { error });
return 0;
}
}
function countItems(nodeData) {
const counts = { input: 0, output: 0 };
if (!nodeData || !Array.isArray(nodeData)) {
return counts;
}
for (const run of nodeData) {
if (run?.data?.main) {
const mainData = run.data.main;
if (Array.isArray(mainData)) {
for (const output of mainData) {
if (Array.isArray(output)) {
counts.output += output.length;
}
}
}
}
}
return counts;
}
function generatePreview(execution) {
const preview = {
totalNodes: 0,
executedNodes: 0,
estimatedSizeKB: 0,
nodes: {},
};
if (!execution.data?.resultData?.runData) {
return {
preview,
recommendation: {
canFetchFull: true,
suggestedMode: 'summary',
reason: 'No execution data available',
},
};
}
const runData = execution.data.resultData.runData;
const nodeNames = Object.keys(runData);
preview.totalNodes = nodeNames.length;
let totalItemsOutput = 0;
let largestNodeItems = 0;
for (const nodeName of nodeNames) {
const nodeData = runData[nodeName];
const itemCounts = countItems(nodeData);
let dataStructure = {};
if (Array.isArray(nodeData) && nodeData.length > 0) {
const firstRun = nodeData[0];
const firstItem = firstRun?.data?.main?.[0]?.[0];
if (firstItem) {
dataStructure = extractStructure(firstItem);
}
}
const nodeSize = estimateDataSize(nodeData);
const nodePreview = {
status: 'success',
itemCounts,
dataStructure,
estimatedSizeKB: nodeSize,
};
if (Array.isArray(nodeData)) {
for (const run of nodeData) {
if (run.error) {
nodePreview.status = 'error';
nodePreview.error = extractErrorMessage(run.error);
break;
}
}
}
preview.nodes[nodeName] = nodePreview;
preview.estimatedSizeKB += nodeSize;
preview.executedNodes++;
totalItemsOutput += itemCounts.output;
largestNodeItems = Math.max(largestNodeItems, itemCounts.output);
}
const recommendation = generateRecommendation(preview.estimatedSizeKB, totalItemsOutput, largestNodeItems);
return { preview, recommendation };
}
function generateRecommendation(totalSizeKB, totalItems, largestNodeItems) {
if (totalSizeKB <= THRESHOLDS.MAX_RECOMMENDED_SIZE_KB && totalItems <= THRESHOLDS.SMALL_DATASET_ITEMS) {
return {
canFetchFull: true,
suggestedMode: 'full',
reason: `Small dataset (${totalSizeKB}KB, ${totalItems} items). Safe to fetch full data.`,
};
}
if (totalSizeKB <= THRESHOLDS.MODERATE_DATASET_SIZE_KB && totalItems <= THRESHOLDS.MODERATE_DATASET_ITEMS) {
return {
canFetchFull: false,
suggestedMode: 'summary',
suggestedItemsLimit: 2,
reason: `Moderate dataset (${totalSizeKB}KB, ${totalItems} items). Summary mode recommended.`,
};
}
const suggestedLimit = Math.max(1, Math.min(5, Math.floor(100 / largestNodeItems)));
return {
canFetchFull: false,
suggestedMode: 'filtered',
suggestedItemsLimit: suggestedLimit,
reason: `Large dataset (${totalSizeKB}KB, ${totalItems} items). Use filtered mode with itemsLimit: ${suggestedLimit}.`,
};
}
function truncateItems(items, limit) {
if (!Array.isArray(items) || items.length === 0) {
return {
truncated: items || [],
metadata: {
totalItems: 0,
itemsShown: 0,
truncated: false,
},
};
}
let totalItems = 0;
for (const output of items) {
if (Array.isArray(output)) {
totalItems += output.length;
}
}
if (limit === 0) {
const structureOnly = items.map(output => {
if (!Array.isArray(output) || output.length === 0) {
return [];
}
return [extractStructure(output[0])];
});
return {
truncated: structureOnly,
metadata: {
totalItems,
itemsShown: 0,
truncated: true,
},
};
}
if (limit < 0) {
return {
truncated: items,
metadata: {
totalItems,
itemsShown: totalItems,
truncated: false,
},
};
}
const result = [];
let itemsShown = 0;
for (const output of items) {
if (!Array.isArray(output)) {
result.push(output);
continue;
}
if (itemsShown >= limit) {
break;
}
const remaining = limit - itemsShown;
const toTake = Math.min(remaining, output.length);
result.push(output.slice(0, toTake));
itemsShown += toTake;
}
return {
truncated: result,
metadata: {
totalItems,
itemsShown,
truncated: itemsShown < totalItems,
},
};
}
function filterExecutionData(execution, options) {
const mode = options.mode || 'summary';
let itemsLimit = options.itemsLimit !== undefined ? options.itemsLimit : 2;
if (itemsLimit !== -1) {
if (itemsLimit < 0) {
logger_1.logger.warn('Invalid itemsLimit, defaulting to 2', { provided: itemsLimit });
itemsLimit = 2;
}
if (itemsLimit > THRESHOLDS.MAX_ITEMS_LIMIT) {
logger_1.logger.warn(`itemsLimit capped at ${THRESHOLDS.MAX_ITEMS_LIMIT}`, { provided: itemsLimit });
itemsLimit = THRESHOLDS.MAX_ITEMS_LIMIT;
}
}
const includeInputData = options.includeInputData || false;
const nodeNamesFilter = options.nodeNames;
const duration = execution.stoppedAt && execution.startedAt
? new Date(execution.stoppedAt).getTime() - new Date(execution.startedAt).getTime()
: undefined;
const response = {
id: execution.id,
workflowId: execution.workflowId,
status: execution.status,
mode,
startedAt: execution.startedAt,
stoppedAt: execution.stoppedAt,
duration,
finished: execution.finished,
};
if (mode === 'preview') {
const { preview, recommendation } = generatePreview(execution);
response.preview = preview;
response.recommendation = recommendation;
return response;
}
if (!execution.data?.resultData?.runData) {
response.summary = {
totalNodes: 0,
executedNodes: 0,
totalItems: 0,
hasMoreData: false,
};
response.nodes = {};
if (execution.data?.resultData?.error) {
response.error = execution.data.resultData.error;
}
return response;
}
const runData = execution.data.resultData.runData;
let nodeNames = Object.keys(runData);
if (nodeNamesFilter && nodeNamesFilter.length > 0) {
nodeNames = nodeNames.filter(name => nodeNamesFilter.includes(name));
}
const processedNodes = {};
let totalItems = 0;
let hasMoreData = false;
for (const nodeName of nodeNames) {
const nodeData = runData[nodeName];
if (!Array.isArray(nodeData) || nodeData.length === 0) {
processedNodes[nodeName] = {
itemsInput: 0,
itemsOutput: 0,
status: 'success',
};
continue;
}
const firstRun = nodeData[0];
const itemCounts = countItems(nodeData);
totalItems += itemCounts.output;
const nodeResult = {
executionTime: firstRun.executionTime,
itemsInput: itemCounts.input,
itemsOutput: itemCounts.output,
status: 'success',
};
if (firstRun.error) {
nodeResult.status = 'error';
nodeResult.error = extractErrorMessage(firstRun.error);
}
if (mode === 'full') {
nodeResult.data = {
output: firstRun.data?.main || [],
metadata: {
totalItems: itemCounts.output,
itemsShown: itemCounts.output,
truncated: false,
},
};
if (includeInputData && firstRun.inputData) {
nodeResult.data.input = firstRun.inputData;
}
}
else {
const outputData = firstRun.data?.main || [];
const { truncated, metadata } = truncateItems(outputData, itemsLimit);
if (metadata.truncated) {
hasMoreData = true;
}
nodeResult.data = {
output: truncated,
metadata,
};
if (includeInputData && firstRun.inputData) {
nodeResult.data.input = firstRun.inputData;
}
}
processedNodes[nodeName] = nodeResult;
}
response.summary = {
totalNodes: Object.keys(runData).length,
executedNodes: nodeNames.length,
totalItems,
hasMoreData,
};
response.nodes = processedNodes;
if (execution.data?.resultData?.error) {
response.error = execution.data.resultData.error;
}
return response;
}
function processExecution(execution, options = {}) {
if (!options.mode && !options.nodeNames && options.itemsLimit === undefined) {
return execution;
}
return filterExecutionData(execution, options);
}
//# sourceMappingURL=execution-processor.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,33 @@
export interface ExpressionFormatIssue {
fieldPath: string;
currentValue: any;
correctedValue: any;
issueType: 'missing-prefix' | 'needs-resource-locator' | 'invalid-rl-structure' | 'mixed-format';
explanation: string;
severity: 'error' | 'warning';
confidence?: number;
}
export interface ResourceLocatorField {
__rl: true;
value: string;
mode: string;
}
export interface ValidationContext {
nodeType: string;
nodeName: string;
nodeId?: string;
}
export declare class ExpressionFormatValidator {
private static readonly VALID_RL_MODES;
private static readonly MAX_RECURSION_DEPTH;
private static readonly EXPRESSION_PREFIX;
private static readonly RESOURCE_LOCATOR_FIELDS;
private static shouldUseResourceLocator;
private static isResourceLocator;
private static generateCorrection;
static validateAndFix(value: any, fieldPath: string, context: ValidationContext): ExpressionFormatIssue | null;
static validateNodeParameters(parameters: any, context: ValidationContext): ExpressionFormatIssue[];
private static validateRecursive;
static formatErrorMessage(issue: ExpressionFormatIssue, context: ValidationContext): string;
}
//# sourceMappingURL=expression-format-validator.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"expression-format-validator.d.ts","sourceRoot":"","sources":["../../src/services/expression-format-validator.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,GAAG,CAAC;IAClB,cAAc,EAAE,GAAG,CAAC;IACpB,SAAS,EAAE,gBAAgB,GAAG,wBAAwB,GAAG,sBAAsB,GAAG,cAAc,CAAC;IACjG,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,IAAI,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,yBAAyB;IACpC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAwD;IAC9F,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAO;IAClD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAO;IAMhD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,uBAAuB,CAmB7C;IAMF,OAAO,CAAC,MAAM,CAAC,wBAAwB;IAoBvC,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAoBhC,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAsBjC,MAAM,CAAC,cAAc,CACnB,KAAK,EAAE,GAAG,EACV,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,iBAAiB,GACzB,qBAAqB,GAAG,IAAI;IAgH/B,MAAM,CAAC,sBAAsB,CAC3B,UAAU,EAAE,GAAG,EACf,OAAO,EAAE,iBAAiB,GACzB,qBAAqB,EAAE;IAY1B,OAAO,CAAC,MAAM,CAAC,iBAAiB;IA0DhC,MAAM,CAAC,kBAAkB,CAAC,KAAK,EAAE,qBAAqB,EAAE,OAAO,EAAE,iBAAiB,GAAG,MAAM;CAoB5F"}

View File

@@ -0,0 +1,209 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExpressionFormatValidator = void 0;
const universal_expression_validator_1 = require("./universal-expression-validator");
const confidence_scorer_1 = require("./confidence-scorer");
class ExpressionFormatValidator {
static shouldUseResourceLocator(fieldName, nodeType) {
const nodeBase = nodeType.split('.').pop()?.toLowerCase() || '';
for (const [pattern, fields] of Object.entries(this.RESOURCE_LOCATOR_FIELDS)) {
if ((nodeBase === pattern || nodeBase.startsWith(`${pattern}-`)) && fields.includes(fieldName)) {
return true;
}
}
return false;
}
static isResourceLocator(value) {
if (typeof value !== 'object' || value === null || value.__rl !== true) {
return false;
}
if (!('value' in value) || !('mode' in value)) {
return false;
}
if (typeof value.mode !== 'string' || !this.VALID_RL_MODES.includes(value.mode)) {
return false;
}
return true;
}
static generateCorrection(value, needsResourceLocator) {
const correctedValue = value.startsWith(this.EXPRESSION_PREFIX)
? value
: `${this.EXPRESSION_PREFIX}${value}`;
if (needsResourceLocator) {
return {
__rl: true,
value: correctedValue,
mode: 'expression'
};
}
return correctedValue;
}
static validateAndFix(value, fieldPath, context) {
if (typeof value !== 'string' && !this.isResourceLocator(value)) {
return null;
}
if (this.isResourceLocator(value)) {
const universalResults = universal_expression_validator_1.UniversalExpressionValidator.validate(value.value);
const invalidResult = universalResults.find(r => !r.isValid && r.needsPrefix);
if (invalidResult) {
return {
fieldPath,
currentValue: value,
correctedValue: {
...value,
value: universal_expression_validator_1.UniversalExpressionValidator.getCorrectedValue(value.value)
},
issueType: 'missing-prefix',
explanation: `Resource locator value: ${invalidResult.explanation}`,
severity: 'error'
};
}
return null;
}
const universalResults = universal_expression_validator_1.UniversalExpressionValidator.validate(value);
const invalidResults = universalResults.filter(r => !r.isValid);
if (invalidResults.length > 0) {
const prefixIssue = invalidResults.find(r => r.needsPrefix);
if (prefixIssue) {
const fieldName = fieldPath.split('.').pop() || '';
const confidenceScore = confidence_scorer_1.ConfidenceScorer.scoreResourceLocatorRecommendation(fieldName, context.nodeType, value);
if (confidenceScore.value >= 0.8) {
return {
fieldPath,
currentValue: value,
correctedValue: this.generateCorrection(value, true),
issueType: 'needs-resource-locator',
explanation: `Field '${fieldName}' contains expression but needs resource locator format with '${this.EXPRESSION_PREFIX}' prefix for evaluation.`,
severity: 'error',
confidence: confidenceScore.value
};
}
else {
return {
fieldPath,
currentValue: value,
correctedValue: universal_expression_validator_1.UniversalExpressionValidator.getCorrectedValue(value),
issueType: 'missing-prefix',
explanation: prefixIssue.explanation,
severity: 'error'
};
}
}
const firstIssue = invalidResults[0];
return {
fieldPath,
currentValue: value,
correctedValue: value,
issueType: 'mixed-format',
explanation: firstIssue.explanation,
severity: 'error'
};
}
const hasExpression = universalResults.some(r => r.hasExpression);
if (hasExpression && typeof value === 'string') {
const fieldName = fieldPath.split('.').pop() || '';
const confidenceScore = confidence_scorer_1.ConfidenceScorer.scoreResourceLocatorRecommendation(fieldName, context.nodeType, value);
if (confidenceScore.value >= 0.5) {
return {
fieldPath,
currentValue: value,
correctedValue: this.generateCorrection(value, true),
issueType: 'needs-resource-locator',
explanation: `Field '${fieldName}' should use resource locator format for better compatibility. (Confidence: ${Math.round(confidenceScore.value * 100)}%)`,
severity: 'warning',
confidence: confidenceScore.value
};
}
}
return null;
}
static validateNodeParameters(parameters, context) {
const issues = [];
const visited = new WeakSet();
this.validateRecursive(parameters, '', context, issues, visited);
return issues;
}
static validateRecursive(obj, path, context, issues, visited, depth = 0) {
if (depth > this.MAX_RECURSION_DEPTH) {
issues.push({
fieldPath: path,
currentValue: obj,
correctedValue: obj,
issueType: 'mixed-format',
explanation: `Maximum recursion depth (${this.MAX_RECURSION_DEPTH}) exceeded. Object may have circular references or be too deeply nested.`,
severity: 'warning'
});
return;
}
if (obj && typeof obj === 'object') {
if (visited.has(obj))
return;
visited.add(obj);
}
const issue = this.validateAndFix(obj, path, context);
if (issue) {
issues.push(issue);
}
if (Array.isArray(obj)) {
obj.forEach((item, index) => {
const newPath = path ? `${path}[${index}]` : `[${index}]`;
this.validateRecursive(item, newPath, context, issues, visited, depth + 1);
});
}
else if (obj && typeof obj === 'object') {
if (this.isResourceLocator(obj)) {
return;
}
Object.entries(obj).forEach(([key, value]) => {
if (key.startsWith('__'))
return;
const newPath = path ? `${path}.${key}` : key;
this.validateRecursive(value, newPath, context, issues, visited, depth + 1);
});
}
}
static formatErrorMessage(issue, context) {
let message = `Expression format ${issue.severity} in node '${context.nodeName}':\n`;
message += `Field '${issue.fieldPath}' ${issue.explanation}\n\n`;
message += `Current (incorrect):\n`;
if (typeof issue.currentValue === 'string') {
message += `"${issue.fieldPath}": "${issue.currentValue}"\n\n`;
}
else {
message += `"${issue.fieldPath}": ${JSON.stringify(issue.currentValue, null, 2)}\n\n`;
}
message += `Fixed (correct):\n`;
if (typeof issue.correctedValue === 'string') {
message += `"${issue.fieldPath}": "${issue.correctedValue}"`;
}
else {
message += `"${issue.fieldPath}": ${JSON.stringify(issue.correctedValue, null, 2)}`;
}
return message;
}
}
exports.ExpressionFormatValidator = ExpressionFormatValidator;
ExpressionFormatValidator.VALID_RL_MODES = ['id', 'url', 'expression', 'name', 'list'];
ExpressionFormatValidator.MAX_RECURSION_DEPTH = 100;
ExpressionFormatValidator.EXPRESSION_PREFIX = '=';
ExpressionFormatValidator.RESOURCE_LOCATOR_FIELDS = {
'github': ['owner', 'repository', 'user', 'organization'],
'googleSheets': ['sheetId', 'documentId', 'spreadsheetId', 'rangeDefinition'],
'googleDrive': ['fileId', 'folderId', 'driveId'],
'slack': ['channel', 'user', 'channelId', 'userId', 'teamId'],
'notion': ['databaseId', 'pageId', 'blockId'],
'airtable': ['baseId', 'tableId', 'viewId'],
'monday': ['boardId', 'itemId', 'groupId'],
'hubspot': ['contactId', 'companyId', 'dealId'],
'salesforce': ['recordId', 'objectName'],
'jira': ['projectKey', 'issueKey', 'boardId'],
'gitlab': ['projectId', 'mergeRequestId', 'issueId'],
'mysql': ['table', 'database', 'schema'],
'postgres': ['table', 'database', 'schema'],
'mongodb': ['collection', 'database'],
's3': ['bucketName', 'key', 'fileName'],
'ftp': ['path', 'fileName'],
'ssh': ['path', 'fileName'],
'redis': ['key'],
};
//# sourceMappingURL=expression-format-validator.js.map

File diff suppressed because one or more lines are too long

27
dist/services/expression-validator.d.ts vendored Normal file
View File

@@ -0,0 +1,27 @@
interface ExpressionValidationResult {
valid: boolean;
errors: string[];
warnings: string[];
usedVariables: Set<string>;
usedNodes: Set<string>;
}
interface ExpressionContext {
availableNodes: string[];
currentNodeName?: string;
isInLoop?: boolean;
hasInputData?: boolean;
}
export declare class ExpressionValidator {
private static readonly EXPRESSION_PATTERN;
private static readonly VARIABLE_PATTERNS;
static validateExpression(expression: string, context: ExpressionContext): ExpressionValidationResult;
private static checkSyntaxErrors;
private static extractExpressions;
private static validateSingleExpression;
private static checkCommonMistakes;
private static checkNodeReferences;
static validateNodeExpressions(parameters: any, context: ExpressionContext): ExpressionValidationResult;
private static validateParametersRecursive;
}
export {};
//# sourceMappingURL=expression-validator.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"expression-validator.d.ts","sourceRoot":"","sources":["../../src/services/expression-validator.ts"],"names":[],"mappings":"AAKA,UAAU,0BAA0B;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACxB;AAED,UAAU,iBAAiB;IACzB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,qBAAa,mBAAmB;IAE9B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAyB;IACnE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAcvC;IAKF,MAAM,CAAC,kBAAkB,CACvB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,iBAAiB,GACzB,0BAA0B;IA2C7B,OAAO,CAAC,MAAM,CAAC,iBAAiB;IA+BhC,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAcjC,OAAO,CAAC,MAAM,CAAC,wBAAwB;IAwEvC,OAAO,CAAC,MAAM,CAAC,mBAAmB;IAkDlC,OAAO,CAAC,MAAM,CAAC,mBAAmB;IAgBlC,MAAM,CAAC,uBAAuB,CAC5B,UAAU,EAAE,GAAG,EACf,OAAO,EAAE,iBAAiB,GACzB,0BAA0B;IAmB7B,OAAO,CAAC,MAAM,CAAC,2BAA2B;CAiD3C"}

187
dist/services/expression-validator.js vendored Normal file
View File

@@ -0,0 +1,187 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExpressionValidator = void 0;
class ExpressionValidator {
static validateExpression(expression, context) {
const result = {
valid: true,
errors: [],
warnings: [],
usedVariables: new Set(),
usedNodes: new Set(),
};
if (!expression) {
return result;
}
if (!context) {
result.valid = false;
result.errors.push('Validation context is required');
return result;
}
const syntaxErrors = this.checkSyntaxErrors(expression);
result.errors.push(...syntaxErrors);
const expressions = this.extractExpressions(expression);
for (const expr of expressions) {
this.validateSingleExpression(expr, context, result);
}
this.checkNodeReferences(result, context);
result.valid = result.errors.length === 0;
return result;
}
static checkSyntaxErrors(expression) {
const errors = [];
const openBrackets = (expression.match(/\{\{/g) || []).length;
const closeBrackets = (expression.match(/\}\}/g) || []).length;
if (openBrackets !== closeBrackets) {
errors.push('Unmatched expression brackets {{ }}');
}
const nestedPattern = /\{\{[^}]*\{\{/;
if (nestedPattern.test(expression)) {
errors.push('Nested expressions are not supported (expression inside another expression)');
}
const emptyExpressionPattern = /\{\{\s*\}\}/;
if (emptyExpressionPattern.test(expression)) {
errors.push('Empty expression found');
}
return errors;
}
static extractExpressions(text) {
const expressions = [];
let match;
while ((match = this.EXPRESSION_PATTERN.exec(text)) !== null) {
expressions.push(match[1].trim());
}
return expressions;
}
static validateSingleExpression(expr, context, result) {
let match;
const jsonPattern = new RegExp(this.VARIABLE_PATTERNS.json.source, this.VARIABLE_PATTERNS.json.flags);
while ((match = jsonPattern.exec(expr)) !== null) {
result.usedVariables.add('$json');
if (!context.hasInputData && !context.isInLoop) {
result.warnings.push('Using $json but node might not have input data');
}
const fullMatch = match[0];
if (fullMatch.includes('.invalid') || fullMatch.includes('.undefined') ||
fullMatch.includes('.null') || fullMatch.includes('.test')) {
result.warnings.push(`Property access '${fullMatch}' looks suspicious - verify this property exists in your data`);
}
}
const nodePattern = new RegExp(this.VARIABLE_PATTERNS.node.source, this.VARIABLE_PATTERNS.node.flags);
while ((match = nodePattern.exec(expr)) !== null) {
const nodeName = match[1];
result.usedNodes.add(nodeName);
result.usedVariables.add('$node');
}
const inputPattern = new RegExp(this.VARIABLE_PATTERNS.input.source, this.VARIABLE_PATTERNS.input.flags);
while ((match = inputPattern.exec(expr)) !== null) {
result.usedVariables.add('$input');
if (!context.hasInputData) {
result.warnings.push('$input is only available when the node has input data');
}
}
const itemsPattern = new RegExp(this.VARIABLE_PATTERNS.items.source, this.VARIABLE_PATTERNS.items.flags);
while ((match = itemsPattern.exec(expr)) !== null) {
const nodeName = match[1];
result.usedNodes.add(nodeName);
result.usedVariables.add('$items');
}
for (const [varName, pattern] of Object.entries(this.VARIABLE_PATTERNS)) {
if (['json', 'node', 'input', 'items'].includes(varName))
continue;
const testPattern = new RegExp(pattern.source, pattern.flags);
if (testPattern.test(expr)) {
result.usedVariables.add(`$${varName}`);
}
}
this.checkCommonMistakes(expr, result);
}
static checkCommonMistakes(expr, result) {
const missingPrefixPattern = /(?<![.$\w['])\b(json|node|input|items|workflow|execution)\b(?!\s*[:''])/;
if (expr.match(missingPrefixPattern)) {
result.warnings.push('Possible missing $ prefix for variable (e.g., use $json instead of json)');
}
if (expr.includes('$json[') && !expr.match(/\$json\[\d+\]/)) {
result.warnings.push('Array access should use numeric index: $json[0] or property access: $json.property');
}
if (expr.match(/\$json\['[^']+'\]/)) {
result.warnings.push("Consider using dot notation: $json.property instead of $json['property']");
}
if (expr.match(/\?\./)) {
result.warnings.push('Optional chaining (?.) is not supported in n8n expressions');
}
if (expr.includes('${')) {
result.errors.push('Template literals ${} are not supported. Use string concatenation instead');
}
}
static checkNodeReferences(result, context) {
for (const nodeName of result.usedNodes) {
if (!context.availableNodes.includes(nodeName)) {
result.errors.push(`Referenced node "${nodeName}" not found in workflow`);
}
}
}
static validateNodeExpressions(parameters, context) {
const combinedResult = {
valid: true,
errors: [],
warnings: [],
usedVariables: new Set(),
usedNodes: new Set(),
};
const visited = new WeakSet();
this.validateParametersRecursive(parameters, context, combinedResult, '', visited);
combinedResult.valid = combinedResult.errors.length === 0;
return combinedResult;
}
static validateParametersRecursive(obj, context, result, path = '', visited = new WeakSet()) {
if (obj && typeof obj === 'object') {
if (visited.has(obj)) {
return;
}
visited.add(obj);
}
if (typeof obj === 'string') {
if (obj.includes('{{')) {
const validation = this.validateExpression(obj, context);
validation.errors.forEach(error => {
result.errors.push(path ? `${path}: ${error}` : error);
});
validation.warnings.forEach(warning => {
result.warnings.push(path ? `${path}: ${warning}` : warning);
});
validation.usedVariables.forEach(v => result.usedVariables.add(v));
validation.usedNodes.forEach(n => result.usedNodes.add(n));
}
}
else if (Array.isArray(obj)) {
obj.forEach((item, index) => {
this.validateParametersRecursive(item, context, result, `${path}[${index}]`, visited);
});
}
else if (obj && typeof obj === 'object') {
Object.entries(obj).forEach(([key, value]) => {
const newPath = path ? `${path}.${key}` : key;
this.validateParametersRecursive(value, context, result, newPath, visited);
});
}
}
}
exports.ExpressionValidator = ExpressionValidator;
ExpressionValidator.EXPRESSION_PATTERN = /\{\{([\s\S]+?)\}\}/g;
ExpressionValidator.VARIABLE_PATTERNS = {
json: /\$json(\.[a-zA-Z_][\w]*|\["[^"]+"\]|\['[^']+'\]|\[\d+\])*/g,
node: /\$node\["([^"]+)"\]\.json/g,
input: /\$input\.item(\.[a-zA-Z_][\w]*|\["[^"]+"\]|\['[^']+'\]|\[\d+\])*/g,
items: /\$items\("([^"]+)"(?:,\s*(-?\d+))?\)/g,
parameter: /\$parameter\["([^"]+)"\]/g,
env: /\$env\.([a-zA-Z_][\w]*)/g,
workflow: /\$workflow\.(id|name|active)/g,
execution: /\$execution\.(id|mode|resumeUrl)/g,
prevNode: /\$prevNode\.(name|outputIndex|runIndex)/g,
itemIndex: /\$itemIndex/g,
runIndex: /\$runIndex/g,
now: /\$now/g,
today: /\$today/g,
};
//# sourceMappingURL=expression-validator.js.map

File diff suppressed because one or more lines are too long

47
dist/services/n8n-api-client.d.ts vendored Normal file
View File

@@ -0,0 +1,47 @@
import { Workflow, WorkflowListParams, WorkflowListResponse, Execution, ExecutionListParams, ExecutionListResponse, Credential, CredentialListParams, CredentialListResponse, Tag, TagListParams, TagListResponse, HealthCheckResponse, N8nVersionInfo, Variable, WebhookRequest, SourceControlStatus, SourceControlPullResult, SourceControlPushResult } from '../types/n8n-api';
export interface N8nApiClientConfig {
baseUrl: string;
apiKey: string;
timeout?: number;
maxRetries?: number;
}
export declare class N8nApiClient {
private client;
private maxRetries;
private baseUrl;
private versionInfo;
private versionFetched;
constructor(config: N8nApiClientConfig);
getVersion(): Promise<N8nVersionInfo | null>;
getCachedVersionInfo(): N8nVersionInfo | null;
healthCheck(): Promise<HealthCheckResponse>;
createWorkflow(workflow: Partial<Workflow>): Promise<Workflow>;
getWorkflow(id: string): Promise<Workflow>;
updateWorkflow(id: string, workflow: Partial<Workflow>): Promise<Workflow>;
deleteWorkflow(id: string): Promise<Workflow>;
activateWorkflow(id: string): Promise<Workflow>;
deactivateWorkflow(id: string): Promise<Workflow>;
listWorkflows(params?: WorkflowListParams): Promise<WorkflowListResponse>;
getExecution(id: string, includeData?: boolean): Promise<Execution>;
listExecutions(params?: ExecutionListParams): Promise<ExecutionListResponse>;
deleteExecution(id: string): Promise<void>;
triggerWebhook(request: WebhookRequest): Promise<any>;
listCredentials(params?: CredentialListParams): Promise<CredentialListResponse>;
getCredential(id: string): Promise<Credential>;
createCredential(credential: Partial<Credential>): Promise<Credential>;
updateCredential(id: string, credential: Partial<Credential>): Promise<Credential>;
deleteCredential(id: string): Promise<void>;
listTags(params?: TagListParams): Promise<TagListResponse>;
createTag(tag: Partial<Tag>): Promise<Tag>;
updateTag(id: string, tag: Partial<Tag>): Promise<Tag>;
deleteTag(id: string): Promise<void>;
getSourceControlStatus(): Promise<SourceControlStatus>;
pullSourceControl(force?: boolean): Promise<SourceControlPullResult>;
pushSourceControl(message: string, fileNames?: string[]): Promise<SourceControlPushResult>;
getVariables(): Promise<Variable[]>;
createVariable(variable: Partial<Variable>): Promise<Variable>;
updateVariable(id: string, variable: Partial<Variable>): Promise<Variable>;
deleteVariable(id: string): Promise<void>;
private validateListResponse;
}
//# sourceMappingURL=n8n-api-client.d.ts.map

1
dist/services/n8n-api-client.d.ts.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"n8n-api-client.d.ts","sourceRoot":"","sources":["../../src/services/n8n-api-client.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,QAAQ,EACR,kBAAkB,EAClB,oBAAoB,EACpB,SAAS,EACT,mBAAmB,EACnB,qBAAqB,EACrB,UAAU,EACV,oBAAoB,EACpB,sBAAsB,EACtB,GAAG,EACH,aAAa,EACb,eAAe,EACf,mBAAmB,EACnB,cAAc,EACd,QAAQ,EACR,cAAc,EAGd,mBAAmB,EACnB,uBAAuB,EACvB,uBAAuB,EACxB,MAAM,kBAAkB,CAAC;AAS1B,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,WAAW,CAA+B;IAClD,OAAO,CAAC,cAAc,CAAS;gBAEnB,MAAM,EAAE,kBAAkB;IAoDhC,UAAU,IAAI,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAgBlD,oBAAoB,IAAI,cAAc,GAAG,IAAI;IAKvC,WAAW,IAAI,OAAO,CAAC,mBAAmB,CAAC;IA6C3C,cAAc,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC;IAU9D,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAS1C,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC;IAsC1E,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAS7C,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAS/C,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAsBjD,aAAa,CAAC,MAAM,GAAE,kBAAuB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAU7E,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,WAAW,UAAQ,GAAG,OAAO,CAAC,SAAS,CAAC;IAwBjE,cAAc,CAAC,MAAM,GAAE,mBAAwB,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAShF,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAS1C,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,GAAG,CAAC;IAiErD,eAAe,CAAC,MAAM,GAAE,oBAAyB,GAAG,OAAO,CAAC,sBAAsB,CAAC;IASnF,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAS9C,gBAAgB,CAAC,UAAU,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC;IAStE,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC;IASlF,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAsB3C,QAAQ,CAAC,MAAM,GAAE,aAAkB,GAAG,OAAO,CAAC,eAAe,CAAC;IAS9D,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC;IAS1C,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC;IAStD,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASpC,sBAAsB,IAAI,OAAO,CAAC,mBAAmB,CAAC;IAStD,iBAAiB,CAAC,KAAK,UAAQ,GAAG,OAAO,CAAC,uBAAuB,CAAC;IASlE,iBAAiB,CACrB,OAAO,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,MAAM,EAAE,GACnB,OAAO,CAAC,uBAAuB,CAAC;IAa7B,YAAY,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;IAWnC,cAAc,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC;IAS9D,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC;IAS1E,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiB/C,OAAO,CAAC,oBAAoB;CAmC7B"}

445
dist/services/n8n-api-client.js vendored Normal file
View File

@@ -0,0 +1,445 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.N8nApiClient = void 0;
const axios_1 = __importDefault(require("axios"));
const logger_1 = require("../utils/logger");
const n8n_errors_1 = require("../utils/n8n-errors");
const n8n_validation_1 = require("./n8n-validation");
const n8n_version_1 = require("./n8n-version");
class N8nApiClient {
constructor(config) {
this.versionInfo = null;
this.versionFetched = false;
const { baseUrl, apiKey, timeout = 30000, maxRetries = 3 } = config;
this.maxRetries = maxRetries;
this.baseUrl = baseUrl;
const apiUrl = baseUrl.endsWith('/api/v1')
? baseUrl
: `${baseUrl.replace(/\/$/, '')}/api/v1`;
this.client = axios_1.default.create({
baseURL: apiUrl,
timeout,
headers: {
'X-N8N-API-KEY': apiKey,
'Content-Type': 'application/json',
},
});
this.client.interceptors.request.use((config) => {
logger_1.logger.debug(`n8n API Request: ${config.method?.toUpperCase()} ${config.url}`, {
params: config.params,
data: config.data,
});
return config;
}, (error) => {
logger_1.logger.error('n8n API Request Error:', error);
return Promise.reject(error);
});
this.client.interceptors.response.use((response) => {
logger_1.logger.debug(`n8n API Response: ${response.status} ${response.config.url}`);
return response;
}, (error) => {
const n8nError = (0, n8n_errors_1.handleN8nApiError)(error);
(0, n8n_errors_1.logN8nError)(n8nError, 'n8n API Response');
return Promise.reject(n8nError);
});
}
async getVersion() {
if (!this.versionFetched) {
this.versionInfo = (0, n8n_version_1.getCachedVersion)(this.baseUrl);
if (!this.versionInfo) {
this.versionInfo = await (0, n8n_version_1.fetchN8nVersion)(this.baseUrl);
}
this.versionFetched = true;
}
return this.versionInfo;
}
getCachedVersionInfo() {
return this.versionInfo;
}
async healthCheck() {
try {
const baseUrl = this.client.defaults.baseURL || '';
const healthzUrl = baseUrl.replace(/\/api\/v\d+\/?$/, '') + '/healthz';
const response = await axios_1.default.get(healthzUrl, {
timeout: 5000,
validateStatus: (status) => status < 500
});
const versionInfo = await this.getVersion();
if (response.status === 200 && response.data?.status === 'ok') {
return {
status: 'ok',
n8nVersion: versionInfo?.version,
features: {}
};
}
throw new Error('healthz endpoint not available');
}
catch (error) {
try {
await this.client.get('/workflows', { params: { limit: 1 } });
const versionInfo = await this.getVersion();
return {
status: 'ok',
n8nVersion: versionInfo?.version,
features: {}
};
}
catch (fallbackError) {
throw (0, n8n_errors_1.handleN8nApiError)(fallbackError);
}
}
}
async createWorkflow(workflow) {
try {
const cleanedWorkflow = (0, n8n_validation_1.cleanWorkflowForCreate)(workflow);
const response = await this.client.post('/workflows', cleanedWorkflow);
return response.data;
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async getWorkflow(id) {
try {
const response = await this.client.get(`/workflows/${id}`);
return response.data;
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async updateWorkflow(id, workflow) {
try {
const cleanedWorkflow = (0, n8n_validation_1.cleanWorkflowForUpdate)(workflow);
const versionInfo = await this.getVersion();
if (versionInfo) {
logger_1.logger.debug(`Updating workflow with n8n version ${versionInfo.version}`);
cleanedWorkflow.settings = (0, n8n_version_1.cleanSettingsForVersion)(cleanedWorkflow.settings, versionInfo);
}
else {
logger_1.logger.warn('Could not determine n8n version, sending all known settings properties');
}
try {
const response = await this.client.put(`/workflows/${id}`, cleanedWorkflow);
return response.data;
}
catch (putError) {
if (putError.response?.status === 405) {
logger_1.logger.debug('PUT method not supported, falling back to PATCH');
const response = await this.client.patch(`/workflows/${id}`, cleanedWorkflow);
return response.data;
}
throw putError;
}
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async deleteWorkflow(id) {
try {
const response = await this.client.delete(`/workflows/${id}`);
return response.data;
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async activateWorkflow(id) {
try {
const response = await this.client.post(`/workflows/${id}/activate`);
return response.data;
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async deactivateWorkflow(id) {
try {
const response = await this.client.post(`/workflows/${id}/deactivate`);
return response.data;
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async listWorkflows(params = {}) {
try {
const response = await this.client.get('/workflows', { params });
return this.validateListResponse(response.data, 'workflows');
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async getExecution(id, includeData = false) {
try {
const response = await this.client.get(`/executions/${id}`, {
params: { includeData },
});
return response.data;
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async listExecutions(params = {}) {
try {
const response = await this.client.get('/executions', { params });
return this.validateListResponse(response.data, 'executions');
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async deleteExecution(id) {
try {
await this.client.delete(`/executions/${id}`);
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async triggerWebhook(request) {
try {
const { webhookUrl, httpMethod, data, headers, waitForResponse = true } = request;
const { SSRFProtection } = await Promise.resolve().then(() => __importStar(require('../utils/ssrf-protection')));
const validation = await SSRFProtection.validateWebhookUrl(webhookUrl);
if (!validation.valid) {
throw new Error(`SSRF protection: ${validation.reason}`);
}
const url = new URL(webhookUrl);
const webhookPath = url.pathname;
const config = {
method: httpMethod,
url: webhookPath,
headers: {
...headers,
'X-N8N-API-KEY': undefined,
},
data: httpMethod !== 'GET' ? data : undefined,
params: httpMethod === 'GET' ? data : undefined,
timeout: waitForResponse ? 120000 : 30000,
};
const webhookClient = axios_1.default.create({
baseURL: new URL('/', webhookUrl).toString(),
validateStatus: (status) => status < 500,
});
const response = await webhookClient.request(config);
return {
status: response.status,
statusText: response.statusText,
data: response.data,
headers: response.headers,
};
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async listCredentials(params = {}) {
try {
const response = await this.client.get('/credentials', { params });
return this.validateListResponse(response.data, 'credentials');
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async getCredential(id) {
try {
const response = await this.client.get(`/credentials/${id}`);
return response.data;
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async createCredential(credential) {
try {
const response = await this.client.post('/credentials', credential);
return response.data;
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async updateCredential(id, credential) {
try {
const response = await this.client.patch(`/credentials/${id}`, credential);
return response.data;
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async deleteCredential(id) {
try {
await this.client.delete(`/credentials/${id}`);
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async listTags(params = {}) {
try {
const response = await this.client.get('/tags', { params });
return this.validateListResponse(response.data, 'tags');
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async createTag(tag) {
try {
const response = await this.client.post('/tags', tag);
return response.data;
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async updateTag(id, tag) {
try {
const response = await this.client.patch(`/tags/${id}`, tag);
return response.data;
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async deleteTag(id) {
try {
await this.client.delete(`/tags/${id}`);
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async getSourceControlStatus() {
try {
const response = await this.client.get('/source-control/status');
return response.data;
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async pullSourceControl(force = false) {
try {
const response = await this.client.post('/source-control/pull', { force });
return response.data;
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async pushSourceControl(message, fileNames) {
try {
const response = await this.client.post('/source-control/push', {
message,
fileNames,
});
return response.data;
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async getVariables() {
try {
const response = await this.client.get('/variables');
return response.data.data || [];
}
catch (error) {
logger_1.logger.warn('Variables API not available, returning empty array');
return [];
}
}
async createVariable(variable) {
try {
const response = await this.client.post('/variables', variable);
return response.data;
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async updateVariable(id, variable) {
try {
const response = await this.client.patch(`/variables/${id}`, variable);
return response.data;
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
async deleteVariable(id) {
try {
await this.client.delete(`/variables/${id}`);
}
catch (error) {
throw (0, n8n_errors_1.handleN8nApiError)(error);
}
}
validateListResponse(responseData, resourceType) {
if (!responseData || typeof responseData !== 'object') {
throw new Error(`Invalid response from n8n API for ${resourceType}: response is not an object`);
}
if (Array.isArray(responseData)) {
logger_1.logger.warn(`n8n API returned array directly instead of {data, nextCursor} object for ${resourceType}. ` +
'Wrapping in expected format for backwards compatibility.');
return {
data: responseData,
nextCursor: null
};
}
if (!Array.isArray(responseData.data)) {
const keys = Object.keys(responseData).slice(0, 5);
const keysPreview = keys.length < Object.keys(responseData).length
? `${keys.join(', ')}...`
: keys.join(', ');
throw new Error(`Invalid response from n8n API for ${resourceType}: expected {data: [], nextCursor?: string}, ` +
`got object with keys: [${keysPreview}]`);
}
return responseData;
}
}
exports.N8nApiClient = N8nApiClient;
//# sourceMappingURL=n8n-api-client.js.map

1
dist/services/n8n-api-client.js.map vendored Normal file

File diff suppressed because one or more lines are too long

273
dist/services/n8n-validation.d.ts vendored Normal file
View File

@@ -0,0 +1,273 @@
import { z } from 'zod';
import { WorkflowNode, WorkflowConnection, Workflow } from '../types/n8n-api';
export declare const workflowNodeSchema: z.ZodObject<{
id: z.ZodString;
name: z.ZodString;
type: z.ZodString;
typeVersion: z.ZodNumber;
position: z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>;
parameters: z.ZodRecord<z.ZodString, z.ZodUnknown>;
credentials: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
disabled: z.ZodOptional<z.ZodBoolean>;
notes: z.ZodOptional<z.ZodString>;
notesInFlow: z.ZodOptional<z.ZodBoolean>;
continueOnFail: z.ZodOptional<z.ZodBoolean>;
retryOnFail: z.ZodOptional<z.ZodBoolean>;
maxTries: z.ZodOptional<z.ZodNumber>;
waitBetweenTries: z.ZodOptional<z.ZodNumber>;
alwaysOutputData: z.ZodOptional<z.ZodBoolean>;
executeOnce: z.ZodOptional<z.ZodBoolean>;
}, "strip", z.ZodTypeAny, {
type: string;
id: string;
name: string;
typeVersion: number;
position: [number, number];
parameters: Record<string, unknown>;
credentials?: Record<string, unknown> | undefined;
retryOnFail?: boolean | undefined;
maxTries?: number | undefined;
waitBetweenTries?: number | undefined;
alwaysOutputData?: boolean | undefined;
continueOnFail?: boolean | undefined;
executeOnce?: boolean | undefined;
disabled?: boolean | undefined;
notes?: string | undefined;
notesInFlow?: boolean | undefined;
}, {
type: string;
id: string;
name: string;
typeVersion: number;
position: [number, number];
parameters: Record<string, unknown>;
credentials?: Record<string, unknown> | undefined;
retryOnFail?: boolean | undefined;
maxTries?: number | undefined;
waitBetweenTries?: number | undefined;
alwaysOutputData?: boolean | undefined;
continueOnFail?: boolean | undefined;
executeOnce?: boolean | undefined;
disabled?: boolean | undefined;
notes?: string | undefined;
notesInFlow?: boolean | undefined;
}>;
export declare const workflowConnectionSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
main: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
node: z.ZodString;
type: z.ZodString;
index: z.ZodNumber;
}, "strip", z.ZodTypeAny, {
type: string;
node: string;
index: number;
}, {
type: string;
node: string;
index: number;
}>, "many">, "many">>;
error: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
node: z.ZodString;
type: z.ZodString;
index: z.ZodNumber;
}, "strip", z.ZodTypeAny, {
type: string;
node: string;
index: number;
}, {
type: string;
node: string;
index: number;
}>, "many">, "many">>;
ai_tool: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
node: z.ZodString;
type: z.ZodString;
index: z.ZodNumber;
}, "strip", z.ZodTypeAny, {
type: string;
node: string;
index: number;
}, {
type: string;
node: string;
index: number;
}>, "many">, "many">>;
ai_languageModel: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
node: z.ZodString;
type: z.ZodString;
index: z.ZodNumber;
}, "strip", z.ZodTypeAny, {
type: string;
node: string;
index: number;
}, {
type: string;
node: string;
index: number;
}>, "many">, "many">>;
ai_memory: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
node: z.ZodString;
type: z.ZodString;
index: z.ZodNumber;
}, "strip", z.ZodTypeAny, {
type: string;
node: string;
index: number;
}, {
type: string;
node: string;
index: number;
}>, "many">, "many">>;
ai_embedding: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
node: z.ZodString;
type: z.ZodString;
index: z.ZodNumber;
}, "strip", z.ZodTypeAny, {
type: string;
node: string;
index: number;
}, {
type: string;
node: string;
index: number;
}>, "many">, "many">>;
ai_vectorStore: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
node: z.ZodString;
type: z.ZodString;
index: z.ZodNumber;
}, "strip", z.ZodTypeAny, {
type: string;
node: string;
index: number;
}, {
type: string;
node: string;
index: number;
}>, "many">, "many">>;
}, "strip", z.ZodTypeAny, {
error?: {
type: string;
node: string;
index: number;
}[][] | undefined;
main?: {
type: string;
node: string;
index: number;
}[][] | undefined;
ai_languageModel?: {
type: string;
node: string;
index: number;
}[][] | undefined;
ai_memory?: {
type: string;
node: string;
index: number;
}[][] | undefined;
ai_tool?: {
type: string;
node: string;
index: number;
}[][] | undefined;
ai_embedding?: {
type: string;
node: string;
index: number;
}[][] | undefined;
ai_vectorStore?: {
type: string;
node: string;
index: number;
}[][] | undefined;
}, {
error?: {
type: string;
node: string;
index: number;
}[][] | undefined;
main?: {
type: string;
node: string;
index: number;
}[][] | undefined;
ai_languageModel?: {
type: string;
node: string;
index: number;
}[][] | undefined;
ai_memory?: {
type: string;
node: string;
index: number;
}[][] | undefined;
ai_tool?: {
type: string;
node: string;
index: number;
}[][] | undefined;
ai_embedding?: {
type: string;
node: string;
index: number;
}[][] | undefined;
ai_vectorStore?: {
type: string;
node: string;
index: number;
}[][] | undefined;
}>>;
export declare const workflowSettingsSchema: z.ZodObject<{
executionOrder: z.ZodDefault<z.ZodEnum<["v0", "v1"]>>;
timezone: z.ZodOptional<z.ZodString>;
saveDataErrorExecution: z.ZodDefault<z.ZodEnum<["all", "none"]>>;
saveDataSuccessExecution: z.ZodDefault<z.ZodEnum<["all", "none"]>>;
saveManualExecutions: z.ZodDefault<z.ZodBoolean>;
saveExecutionProgress: z.ZodDefault<z.ZodBoolean>;
executionTimeout: z.ZodOptional<z.ZodNumber>;
errorWorkflow: z.ZodOptional<z.ZodString>;
callerPolicy: z.ZodOptional<z.ZodEnum<["any", "workflowsFromSameOwner", "workflowsFromAList"]>>;
availableInMCP: z.ZodOptional<z.ZodBoolean>;
}, "strip", z.ZodTypeAny, {
executionOrder: "v0" | "v1";
saveDataErrorExecution: "all" | "none";
saveDataSuccessExecution: "all" | "none";
saveManualExecutions: boolean;
saveExecutionProgress: boolean;
timezone?: string | undefined;
executionTimeout?: number | undefined;
errorWorkflow?: string | undefined;
callerPolicy?: "any" | "workflowsFromSameOwner" | "workflowsFromAList" | undefined;
availableInMCP?: boolean | undefined;
}, {
timezone?: string | undefined;
executionOrder?: "v0" | "v1" | undefined;
saveDataErrorExecution?: "all" | "none" | undefined;
saveDataSuccessExecution?: "all" | "none" | undefined;
saveManualExecutions?: boolean | undefined;
saveExecutionProgress?: boolean | undefined;
executionTimeout?: number | undefined;
errorWorkflow?: string | undefined;
callerPolicy?: "any" | "workflowsFromSameOwner" | "workflowsFromAList" | undefined;
availableInMCP?: boolean | undefined;
}>;
export declare const defaultWorkflowSettings: {
executionOrder: "v1";
saveDataErrorExecution: "all";
saveDataSuccessExecution: "all";
saveManualExecutions: boolean;
saveExecutionProgress: boolean;
};
export declare function validateWorkflowNode(node: unknown): WorkflowNode;
export declare function validateWorkflowConnections(connections: unknown): WorkflowConnection;
export declare function validateWorkflowSettings(settings: unknown): z.infer<typeof workflowSettingsSchema>;
export declare function cleanWorkflowForCreate(workflow: Partial<Workflow>): Partial<Workflow>;
export declare function cleanWorkflowForUpdate(workflow: Workflow): Partial<Workflow>;
export declare function validateWorkflowStructure(workflow: Partial<Workflow>): string[];
export declare function hasWebhookTrigger(workflow: Workflow): boolean;
export declare function validateFilterBasedNodeMetadata(node: WorkflowNode): string[];
export declare function validateOperatorStructure(operator: any, path: string): string[];
export declare function getWebhookUrl(workflow: Workflow): string | null;
export declare function getWorkflowStructureExample(): string;
export declare function getWorkflowFixSuggestions(errors: string[]): string[];
//# sourceMappingURL=n8n-validation.d.ts.map

1
dist/services/n8n-validation.d.ts.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"n8n-validation.d.ts","sourceRoot":"","sources":["../../src/services/n8n-validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAM9E,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiB7B,CAAC;AAkBH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAUpC,CAAC;AAEF,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAWjC,CAAC;AAGH,eAAO,MAAM,uBAAuB;;;;;;CAMnC,CAAC;AAGF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,OAAO,GAAG,YAAY,CAEhE;AAED,wBAAgB,2BAA2B,CAAC,WAAW,EAAE,OAAO,GAAG,kBAAkB,CAEpF;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAElG;AAGD,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,CAsBrF;AAiBD,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CA4D5E;AAGD,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,MAAM,EAAE,CAyP/E;AAGD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAK7D;AAMD,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,EAAE,CA+F5E;AAMD,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CA0D/E;AAGD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,GAAG,IAAI,CAmB/D;AAGD,wBAAgB,2BAA2B,IAAI,MAAM,CA6CpD;AAGD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAmBpE"}

481
dist/services/n8n-validation.js vendored Normal file
View File

@@ -0,0 +1,481 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.defaultWorkflowSettings = exports.workflowSettingsSchema = exports.workflowConnectionSchema = exports.workflowNodeSchema = void 0;
exports.validateWorkflowNode = validateWorkflowNode;
exports.validateWorkflowConnections = validateWorkflowConnections;
exports.validateWorkflowSettings = validateWorkflowSettings;
exports.cleanWorkflowForCreate = cleanWorkflowForCreate;
exports.cleanWorkflowForUpdate = cleanWorkflowForUpdate;
exports.validateWorkflowStructure = validateWorkflowStructure;
exports.hasWebhookTrigger = hasWebhookTrigger;
exports.validateFilterBasedNodeMetadata = validateFilterBasedNodeMetadata;
exports.validateOperatorStructure = validateOperatorStructure;
exports.getWebhookUrl = getWebhookUrl;
exports.getWorkflowStructureExample = getWorkflowStructureExample;
exports.getWorkflowFixSuggestions = getWorkflowFixSuggestions;
const zod_1 = require("zod");
const node_type_utils_1 = require("../utils/node-type-utils");
const node_classification_1 = require("../utils/node-classification");
exports.workflowNodeSchema = zod_1.z.object({
id: zod_1.z.string(),
name: zod_1.z.string(),
type: zod_1.z.string(),
typeVersion: zod_1.z.number(),
position: zod_1.z.tuple([zod_1.z.number(), zod_1.z.number()]),
parameters: zod_1.z.record(zod_1.z.unknown()),
credentials: zod_1.z.record(zod_1.z.unknown()).optional(),
disabled: zod_1.z.boolean().optional(),
notes: zod_1.z.string().optional(),
notesInFlow: zod_1.z.boolean().optional(),
continueOnFail: zod_1.z.boolean().optional(),
retryOnFail: zod_1.z.boolean().optional(),
maxTries: zod_1.z.number().optional(),
waitBetweenTries: zod_1.z.number().optional(),
alwaysOutputData: zod_1.z.boolean().optional(),
executeOnce: zod_1.z.boolean().optional(),
});
const connectionArraySchema = zod_1.z.array(zod_1.z.array(zod_1.z.object({
node: zod_1.z.string(),
type: zod_1.z.string(),
index: zod_1.z.number(),
})));
exports.workflowConnectionSchema = zod_1.z.record(zod_1.z.object({
main: connectionArraySchema.optional(),
error: connectionArraySchema.optional(),
ai_tool: connectionArraySchema.optional(),
ai_languageModel: connectionArraySchema.optional(),
ai_memory: connectionArraySchema.optional(),
ai_embedding: connectionArraySchema.optional(),
ai_vectorStore: connectionArraySchema.optional(),
}));
exports.workflowSettingsSchema = zod_1.z.object({
executionOrder: zod_1.z.enum(['v0', 'v1']).default('v1'),
timezone: zod_1.z.string().optional(),
saveDataErrorExecution: zod_1.z.enum(['all', 'none']).default('all'),
saveDataSuccessExecution: zod_1.z.enum(['all', 'none']).default('all'),
saveManualExecutions: zod_1.z.boolean().default(true),
saveExecutionProgress: zod_1.z.boolean().default(true),
executionTimeout: zod_1.z.number().optional(),
errorWorkflow: zod_1.z.string().optional(),
callerPolicy: zod_1.z.enum(['any', 'workflowsFromSameOwner', 'workflowsFromAList']).optional(),
availableInMCP: zod_1.z.boolean().optional(),
});
exports.defaultWorkflowSettings = {
executionOrder: 'v1',
saveDataErrorExecution: 'all',
saveDataSuccessExecution: 'all',
saveManualExecutions: true,
saveExecutionProgress: true,
};
function validateWorkflowNode(node) {
return exports.workflowNodeSchema.parse(node);
}
function validateWorkflowConnections(connections) {
return exports.workflowConnectionSchema.parse(connections);
}
function validateWorkflowSettings(settings) {
return exports.workflowSettingsSchema.parse(settings);
}
function cleanWorkflowForCreate(workflow) {
const { id, createdAt, updatedAt, versionId, meta, active, tags, ...cleanedWorkflow } = workflow;
if (!cleanedWorkflow.settings || Object.keys(cleanedWorkflow.settings).length === 0) {
cleanedWorkflow.settings = exports.defaultWorkflowSettings;
}
return cleanedWorkflow;
}
function cleanWorkflowForUpdate(workflow) {
const { id, createdAt, updatedAt, versionId, versionCounter, meta, staticData, pinData, tags, description, isArchived, usedCredentials, sharedWithProjects, triggerCount, shared, active, activeVersionId, activeVersion, ...cleanedWorkflow } = workflow;
const ALL_KNOWN_SETTINGS_PROPERTIES = new Set([
'saveExecutionProgress',
'saveManualExecutions',
'saveDataErrorExecution',
'saveDataSuccessExecution',
'executionTimeout',
'errorWorkflow',
'timezone',
'executionOrder',
'callerPolicy',
'callerIds',
'timeSavedPerExecution',
'availableInMCP',
]);
if (cleanedWorkflow.settings && typeof cleanedWorkflow.settings === 'object') {
const filteredSettings = {};
for (const [key, value] of Object.entries(cleanedWorkflow.settings)) {
if (ALL_KNOWN_SETTINGS_PROPERTIES.has(key)) {
filteredSettings[key] = value;
}
}
cleanedWorkflow.settings = filteredSettings;
}
else {
cleanedWorkflow.settings = {};
}
return cleanedWorkflow;
}
function validateWorkflowStructure(workflow) {
const errors = [];
if (!workflow.name) {
errors.push('Workflow name is required');
}
if (!workflow.nodes || workflow.nodes.length === 0) {
errors.push('Workflow must have at least one node');
}
if (workflow.nodes && workflow.nodes.length > 0) {
const hasExecutableNodes = workflow.nodes.some(node => !(0, node_classification_1.isNonExecutableNode)(node.type));
if (!hasExecutableNodes) {
errors.push('Workflow must have at least one executable node. Sticky notes alone cannot form a valid workflow.');
}
}
if (!workflow.connections) {
errors.push('Workflow connections are required');
}
if (workflow.nodes && workflow.nodes.length === 1) {
const singleNode = workflow.nodes[0];
const isWebhookOnly = singleNode.type === 'n8n-nodes-base.webhook' ||
singleNode.type === 'n8n-nodes-base.webhookTrigger';
if (!isWebhookOnly) {
errors.push(`Single non-webhook node workflow is invalid. Current node: "${singleNode.name}" (${singleNode.type}). Add another node using: {type: 'addNode', node: {name: 'Process Data', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [450, 300], parameters: {}}}`);
}
}
if (workflow.nodes && workflow.nodes.length > 1 && workflow.connections) {
const executableNodes = workflow.nodes.filter(node => !(0, node_classification_1.isNonExecutableNode)(node.type));
const connectionCount = Object.keys(workflow.connections).length;
if (connectionCount === 0 && executableNodes.length > 1) {
const nodeNames = executableNodes.slice(0, 2).map(n => n.name);
errors.push(`Multi-node workflow has no connections between nodes. Add a connection using: {type: 'addConnection', source: '${nodeNames[0]}', target: '${nodeNames[1]}', sourcePort: 'main', targetPort: 'main'}`);
}
else if (connectionCount > 0 || executableNodes.length > 1) {
const connectedNodes = new Set();
Object.entries(workflow.connections).forEach(([sourceName, connection]) => {
connectedNodes.add(sourceName);
if (connection.main && Array.isArray(connection.main)) {
connection.main.forEach((outputs) => {
if (Array.isArray(outputs)) {
outputs.forEach((target) => {
connectedNodes.add(target.node);
});
}
});
}
});
const disconnectedNodes = workflow.nodes.filter(node => {
if ((0, node_classification_1.isNonExecutableNode)(node.type)) {
return false;
}
const isConnected = connectedNodes.has(node.name);
const isNodeTrigger = (0, node_type_utils_1.isTriggerNode)(node.type);
if (isNodeTrigger) {
return !workflow.connections?.[node.name];
}
return !isConnected;
});
if (disconnectedNodes.length > 0) {
const disconnectedList = disconnectedNodes.map(n => `"${n.name}" (${n.type})`).join(', ');
const firstDisconnected = disconnectedNodes[0];
const suggestedSource = workflow.nodes.find(n => connectedNodes.has(n.name))?.name || workflow.nodes[0].name;
errors.push(`Disconnected nodes detected: ${disconnectedList}. Each node must have at least one connection. Add a connection: {type: 'addConnection', source: '${suggestedSource}', target: '${firstDisconnected.name}', sourcePort: 'main', targetPort: 'main'}`);
}
}
}
if (workflow.nodes) {
workflow.nodes.forEach((node, index) => {
try {
validateWorkflowNode(node);
if (node.type.startsWith('nodes-base.')) {
errors.push(`Invalid node type "${node.type}" at index ${index}. Use "n8n-nodes-base.${node.type.substring(11)}" instead.`);
}
else if (!node.type.includes('.')) {
errors.push(`Invalid node type "${node.type}" at index ${index}. Node types must include package prefix (e.g., "n8n-nodes-base.webhook").`);
}
}
catch (error) {
errors.push(`Invalid node at index ${index}: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
});
}
if (workflow.nodes) {
workflow.nodes.forEach((node, index) => {
const filterErrors = validateFilterBasedNodeMetadata(node);
if (filterErrors.length > 0) {
errors.push(...filterErrors.map(err => `Node "${node.name}" (index ${index}): ${err}`));
}
});
}
if (workflow.connections) {
try {
validateWorkflowConnections(workflow.connections);
}
catch (error) {
errors.push(`Invalid connections: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
if (workflow.active === true && workflow.nodes && workflow.nodes.length > 0) {
const activatableTriggers = workflow.nodes.filter(node => !node.disabled && (0, node_type_utils_1.isActivatableTrigger)(node.type));
const executeWorkflowTriggers = workflow.nodes.filter(node => !node.disabled && node.type.toLowerCase().includes('executeworkflow'));
if (activatableTriggers.length === 0 && executeWorkflowTriggers.length > 0) {
const triggerNames = executeWorkflowTriggers.map(n => n.name).join(', ');
errors.push(`Cannot activate workflow with only Execute Workflow Trigger nodes (${triggerNames}). ` +
'Execute Workflow Trigger can only be invoked by other workflows, not activated. ' +
'Either deactivate the workflow or add a webhook/schedule/polling trigger.');
}
}
if (workflow.nodes && workflow.connections) {
const switchNodes = workflow.nodes.filter(n => {
if (n.type !== 'n8n-nodes-base.switch')
return false;
const mode = n.parameters?.mode;
return !mode || mode === 'rules';
});
for (const switchNode of switchNodes) {
const params = switchNode.parameters;
const rules = params?.rules?.rules || [];
const nodeConnections = workflow.connections[switchNode.name];
if (rules.length > 0 && nodeConnections?.main) {
const outputBranches = nodeConnections.main.length;
if (outputBranches !== rules.length) {
const ruleNames = rules.map((r, i) => r.outputKey ? `"${r.outputKey}" (index ${i})` : `Rule ${i}`).join(', ');
errors.push(`Switch node "${switchNode.name}" has ${rules.length} rules [${ruleNames}] ` +
`but only ${outputBranches} output branch${outputBranches !== 1 ? 'es' : ''} in connections. ` +
`Each rule needs its own output branch. When connecting to Switch outputs, specify sourceIndex: ` +
rules.map((_, i) => i).join(', ') +
` (or use case parameter for clarity).`);
}
const nonEmptyBranches = nodeConnections.main.filter((branch) => branch.length > 0).length;
if (nonEmptyBranches < rules.length) {
const emptyIndices = nodeConnections.main
.map((branch, i) => branch.length === 0 ? i : -1)
.filter((i) => i !== -1 && i < rules.length);
if (emptyIndices.length > 0) {
const ruleInfo = emptyIndices.map((i) => {
const rule = rules[i];
return rule.outputKey ? `"${rule.outputKey}" (index ${i})` : `Rule ${i}`;
}).join(', ');
errors.push(`Switch node "${switchNode.name}" has unconnected output${emptyIndices.length !== 1 ? 's' : ''}: ${ruleInfo}. ` +
`Add connection${emptyIndices.length !== 1 ? 's' : ''} using sourceIndex: ${emptyIndices.join(' or ')}.`);
}
}
}
}
}
if (workflow.nodes && workflow.connections) {
const nodeNames = new Set(workflow.nodes.map(node => node.name));
const nodeIds = new Set(workflow.nodes.map(node => node.id));
const nodeIdToName = new Map(workflow.nodes.map(node => [node.id, node.name]));
Object.entries(workflow.connections).forEach(([sourceName, connection]) => {
if (!nodeNames.has(sourceName)) {
if (nodeIds.has(sourceName)) {
const correctName = nodeIdToName.get(sourceName);
errors.push(`Connection uses node ID '${sourceName}' but must use node name '${correctName}'. Change connections.${sourceName} to connections['${correctName}']`);
}
else {
errors.push(`Connection references non-existent node: ${sourceName}`);
}
}
if (connection.main && Array.isArray(connection.main)) {
connection.main.forEach((outputs, outputIndex) => {
if (Array.isArray(outputs)) {
outputs.forEach((target, targetIndex) => {
if (!nodeNames.has(target.node)) {
if (nodeIds.has(target.node)) {
const correctName = nodeIdToName.get(target.node);
errors.push(`Connection target uses node ID '${target.node}' but must use node name '${correctName}' (from ${sourceName}[${outputIndex}][${targetIndex}])`);
}
else {
errors.push(`Connection references non-existent target node: ${target.node} (from ${sourceName}[${outputIndex}][${targetIndex}])`);
}
}
});
}
});
}
});
}
return errors;
}
function hasWebhookTrigger(workflow) {
return workflow.nodes.some(node => node.type === 'n8n-nodes-base.webhook' ||
node.type === 'n8n-nodes-base.webhookTrigger');
}
function validateFilterBasedNodeMetadata(node) {
const errors = [];
const isIFNode = node.type === 'n8n-nodes-base.if' && node.typeVersion >= 2.2;
const isSwitchNode = node.type === 'n8n-nodes-base.switch' && node.typeVersion >= 3.2;
if (!isIFNode && !isSwitchNode) {
return errors;
}
if (isIFNode) {
const conditions = node.parameters.conditions;
if (!conditions?.options) {
errors.push('Missing required "conditions.options". ' +
'IF v2.2+ requires: {version: 2, leftValue: "", caseSensitive: true, typeValidation: "strict"}');
}
else {
const requiredFields = {
version: 2,
leftValue: '',
caseSensitive: 'boolean',
typeValidation: 'strict'
};
for (const [field, expectedValue] of Object.entries(requiredFields)) {
if (!(field in conditions.options)) {
errors.push(`Missing required field "conditions.options.${field}". ` +
`Expected value: ${typeof expectedValue === 'string' ? `"${expectedValue}"` : expectedValue}`);
}
}
}
if (conditions?.conditions && Array.isArray(conditions.conditions)) {
conditions.conditions.forEach((condition, i) => {
const operatorErrors = validateOperatorStructure(condition.operator, `conditions.conditions[${i}].operator`);
errors.push(...operatorErrors);
});
}
}
if (isSwitchNode) {
const rules = node.parameters.rules;
if (rules?.rules && Array.isArray(rules.rules)) {
rules.rules.forEach((rule, ruleIndex) => {
if (!rule.conditions?.options) {
errors.push(`Missing required "rules.rules[${ruleIndex}].conditions.options". ` +
'Switch v3.2+ requires: {version: 2, leftValue: "", caseSensitive: true, typeValidation: "strict"}');
}
else {
const requiredFields = {
version: 2,
leftValue: '',
caseSensitive: 'boolean',
typeValidation: 'strict'
};
for (const [field, expectedValue] of Object.entries(requiredFields)) {
if (!(field in rule.conditions.options)) {
errors.push(`Missing required field "rules.rules[${ruleIndex}].conditions.options.${field}". ` +
`Expected value: ${typeof expectedValue === 'string' ? `"${expectedValue}"` : expectedValue}`);
}
}
}
if (rule.conditions?.conditions && Array.isArray(rule.conditions.conditions)) {
rule.conditions.conditions.forEach((condition, condIndex) => {
const operatorErrors = validateOperatorStructure(condition.operator, `rules.rules[${ruleIndex}].conditions.conditions[${condIndex}].operator`);
errors.push(...operatorErrors);
});
}
});
}
}
return errors;
}
function validateOperatorStructure(operator, path) {
const errors = [];
if (!operator || typeof operator !== 'object') {
errors.push(`${path}: operator is missing or not an object`);
return errors;
}
if (!operator.type) {
errors.push(`${path}: missing required field "type". ` +
'Must be a data type: "string", "number", "boolean", "dateTime", "array", or "object"');
}
else {
const validTypes = ['string', 'number', 'boolean', 'dateTime', 'array', 'object'];
if (!validTypes.includes(operator.type)) {
errors.push(`${path}: invalid type "${operator.type}". ` +
`Type must be a data type (${validTypes.join(', ')}), not an operation name. ` +
'Did you mean to use the "operation" field?');
}
}
if (!operator.operation) {
errors.push(`${path}: missing required field "operation". ` +
'Operation specifies the comparison type (e.g., "equals", "contains", "isNotEmpty")');
}
if (operator.operation) {
const unaryOperators = ['isEmpty', 'isNotEmpty', 'true', 'false', 'isNumeric'];
const isUnary = unaryOperators.includes(operator.operation);
if (isUnary) {
if (operator.singleValue !== true) {
errors.push(`${path}: unary operator "${operator.operation}" requires "singleValue: true". ` +
'Unary operators do not use rightValue.');
}
}
else {
if (operator.singleValue === true) {
errors.push(`${path}: binary operator "${operator.operation}" should not have "singleValue: true". ` +
'Only unary operators (isEmpty, isNotEmpty, true, false, isNumeric) need this property.');
}
}
}
return errors;
}
function getWebhookUrl(workflow) {
const webhookNode = workflow.nodes.find(node => node.type === 'n8n-nodes-base.webhook' ||
node.type === 'n8n-nodes-base.webhookTrigger');
if (!webhookNode || !webhookNode.parameters) {
return null;
}
const path = webhookNode.parameters.path;
if (!path) {
return null;
}
return path;
}
function getWorkflowStructureExample() {
return `
Minimal Workflow Example:
{
"name": "My Workflow",
"nodes": [
{
"id": "manual-trigger-1",
"name": "Manual Trigger",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [250, 300],
"parameters": {}
},
{
"id": "set-1",
"name": "Set Data",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [450, 300],
"parameters": {
"mode": "manual",
"assignments": {
"assignments": [{
"id": "1",
"name": "message",
"value": "Hello World",
"type": "string"
}]
}
}
}
],
"connections": {
"Manual Trigger": {
"main": [[{
"node": "Set Data",
"type": "main",
"index": 0
}]]
}
}
}
IMPORTANT: In connections, use the node NAME (e.g., "Manual Trigger"), NOT the node ID or type!`;
}
function getWorkflowFixSuggestions(errors) {
const suggestions = [];
if (errors.some(e => e.includes('empty connections'))) {
suggestions.push('Add connections between your nodes. Each node (except endpoints) should connect to another node.');
suggestions.push('Connection format: connections: { "Source Node Name": { "main": [[{ "node": "Target Node Name", "type": "main", "index": 0 }]] } }');
}
if (errors.some(e => e.includes('Single-node workflows'))) {
suggestions.push('Add at least one more node to process data. Common patterns: Trigger → Process → Output');
suggestions.push('Examples: Manual Trigger → Set, Webhook → HTTP Request, Schedule Trigger → Database Query');
}
if (errors.some(e => e.includes('node ID') && e.includes('instead of node name'))) {
suggestions.push('Replace node IDs with node names in connections. The name is what appears in the node header.');
suggestions.push('Wrong: connections: { "set-1": {...} }, Right: connections: { "Set Data": {...} }');
}
return suggestions;
}
//# sourceMappingURL=n8n-validation.js.map

1
dist/services/n8n-validation.js.map vendored Normal file

File diff suppressed because one or more lines are too long

23
dist/services/n8n-version.d.ts vendored Normal file
View File

@@ -0,0 +1,23 @@
import { N8nVersionInfo } from '../types/n8n-api';
export declare function parseVersion(versionString: string): N8nVersionInfo | null;
export declare function compareVersions(a: N8nVersionInfo, b: N8nVersionInfo): number;
export declare function versionAtLeast(version: N8nVersionInfo, major: number, minor: number, patch?: number): boolean;
export declare function getSupportedSettingsProperties(version: N8nVersionInfo): Set<string>;
export declare function fetchN8nVersion(baseUrl: string): Promise<N8nVersionInfo | null>;
export declare function clearVersionCache(): void;
export declare function getCachedVersion(baseUrl: string): N8nVersionInfo | null;
export declare function setCachedVersion(baseUrl: string, version: N8nVersionInfo): void;
export declare function cleanSettingsForVersion(settings: Record<string, unknown> | undefined, version: N8nVersionInfo | null): Record<string, unknown>;
export declare const VERSION_THRESHOLDS: {
EXECUTION_ORDER: {
major: number;
minor: number;
patch: number;
};
CALLER_POLICY: {
major: number;
minor: number;
patch: number;
};
};
//# sourceMappingURL=n8n-version.d.ts.map

1
dist/services/n8n-version.d.ts.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"n8n-version.d.ts","sourceRoot":"","sources":["../../src/services/n8n-version.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAE,cAAc,EAAuB,MAAM,kBAAkB,CAAC;AAkCvE,wBAAgB,YAAY,CAAC,aAAa,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAazE;AAKD,wBAAgB,eAAe,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,cAAc,GAAG,MAAM,CAI5E;AAKD,wBAAgB,cAAc,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG,OAAO,CAGxG;AAKD,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,cAAc,GAAG,GAAG,CAAC,MAAM,CAAC,CAcnF;AASD,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAgDrF;AAKD,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAKD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAEvE;AAKD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAE/E;AAYD,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC7C,OAAO,EAAE,cAAc,GAAG,IAAI,GAC7B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAsBzB;AAGD,eAAO,MAAM,kBAAkB;;;;;;;;;;;CAG9B,CAAC"}

142
dist/services/n8n-version.js vendored Normal file
View File

@@ -0,0 +1,142 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.VERSION_THRESHOLDS = void 0;
exports.parseVersion = parseVersion;
exports.compareVersions = compareVersions;
exports.versionAtLeast = versionAtLeast;
exports.getSupportedSettingsProperties = getSupportedSettingsProperties;
exports.fetchN8nVersion = fetchN8nVersion;
exports.clearVersionCache = clearVersionCache;
exports.getCachedVersion = getCachedVersion;
exports.setCachedVersion = setCachedVersion;
exports.cleanSettingsForVersion = cleanSettingsForVersion;
const axios_1 = __importDefault(require("axios"));
const logger_1 = require("../utils/logger");
const versionCache = new Map();
const SETTINGS_BY_VERSION = {
core: [
'saveExecutionProgress',
'saveManualExecutions',
'saveDataErrorExecution',
'saveDataSuccessExecution',
'executionTimeout',
'errorWorkflow',
'timezone',
],
v1_37_0: [
'executionOrder',
],
v1_119_0: [
'callerPolicy',
'callerIds',
'timeSavedPerExecution',
'availableInMCP',
],
};
function parseVersion(versionString) {
const match = versionString.match(/^(\d+)\.(\d+)\.(\d+)/);
if (!match) {
return null;
}
return {
version: versionString,
major: parseInt(match[1], 10),
minor: parseInt(match[2], 10),
patch: parseInt(match[3], 10),
};
}
function compareVersions(a, b) {
if (a.major !== b.major)
return a.major - b.major;
if (a.minor !== b.minor)
return a.minor - b.minor;
return a.patch - b.patch;
}
function versionAtLeast(version, major, minor, patch = 0) {
const target = { version: '', major, minor, patch };
return compareVersions(version, target) >= 0;
}
function getSupportedSettingsProperties(version) {
const supported = new Set(SETTINGS_BY_VERSION.core);
if (versionAtLeast(version, 1, 37, 0)) {
SETTINGS_BY_VERSION.v1_37_0.forEach(prop => supported.add(prop));
}
if (versionAtLeast(version, 1, 119, 0)) {
SETTINGS_BY_VERSION.v1_119_0.forEach(prop => supported.add(prop));
}
return supported;
}
async function fetchN8nVersion(baseUrl) {
const cached = versionCache.get(baseUrl);
if (cached) {
logger_1.logger.debug(`Using cached n8n version for ${baseUrl}: ${cached.version}`);
return cached;
}
try {
const cleanBaseUrl = baseUrl.replace(/\/api\/v\d+\/?$/, '').replace(/\/$/, '');
const settingsUrl = `${cleanBaseUrl}/rest/settings`;
logger_1.logger.debug(`Fetching n8n version from ${settingsUrl}`);
const response = await axios_1.default.get(settingsUrl, {
timeout: 5000,
validateStatus: (status) => status < 500,
});
if (response.status === 200 && response.data) {
const settings = response.data.data;
if (!settings) {
logger_1.logger.warn('No data in settings response');
return null;
}
const versionString = settings.n8nVersion || settings.versionCli;
if (versionString) {
const versionInfo = parseVersion(versionString);
if (versionInfo) {
versionCache.set(baseUrl, versionInfo);
logger_1.logger.debug(`Detected n8n version: ${versionInfo.version}`);
return versionInfo;
}
}
}
logger_1.logger.warn(`Could not determine n8n version from ${settingsUrl}`);
return null;
}
catch (error) {
logger_1.logger.warn(`Failed to fetch n8n version: ${error instanceof Error ? error.message : 'Unknown error'}`);
return null;
}
}
function clearVersionCache() {
versionCache.clear();
}
function getCachedVersion(baseUrl) {
return versionCache.get(baseUrl) || null;
}
function setCachedVersion(baseUrl, version) {
versionCache.set(baseUrl, version);
}
function cleanSettingsForVersion(settings, version) {
if (!settings || typeof settings !== 'object') {
return {};
}
if (!version) {
return settings;
}
const supportedProperties = getSupportedSettingsProperties(version);
const cleaned = {};
for (const [key, value] of Object.entries(settings)) {
if (supportedProperties.has(key)) {
cleaned[key] = value;
}
else {
logger_1.logger.debug(`Filtered out unsupported settings property: ${key} (n8n ${version.version})`);
}
}
return cleaned;
}
exports.VERSION_THRESHOLDS = {
EXECUTION_ORDER: { major: 1, minor: 37, patch: 0 },
CALLER_POLICY: { major: 1, minor: 119, patch: 0 },
};
//# sourceMappingURL=n8n-version.js.map

1
dist/services/n8n-version.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"n8n-version.js","sourceRoot":"","sources":["../../src/services/n8n-version.ts"],"names":[],"mappings":";;;;;;AAsDA,oCAaC;AAKD,0CAIC;AAKD,wCAGC;AAKD,wEAcC;AASD,0CAgDC;AAKD,8CAEC;AAKD,4CAEC;AAKD,4CAEC;AAYD,0DAyBC;AAxMD,kDAA0B;AAC1B,4CAAyC;AAIzC,MAAM,YAAY,GAAG,IAAI,GAAG,EAA0B,CAAC;AAIvD,MAAM,mBAAmB,GAAG;IAE1B,IAAI,EAAE;QACJ,uBAAuB;QACvB,sBAAsB;QACtB,wBAAwB;QACxB,0BAA0B;QAC1B,kBAAkB;QAClB,eAAe;QACf,UAAU;KACX;IAED,OAAO,EAAE;QACP,gBAAgB;KACjB;IAED,QAAQ,EAAE;QACR,cAAc;QACd,WAAW;QACX,uBAAuB;QACvB,gBAAgB;KACjB;CACF,CAAC;AAKF,SAAgB,YAAY,CAAC,aAAqB;IAEhD,MAAM,KAAK,GAAG,aAAa,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAC;IAC1D,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO;QACL,OAAO,EAAE,aAAa;QACtB,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QAC7B,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QAC7B,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;KAC9B,CAAC;AACJ,CAAC;AAKD,SAAgB,eAAe,CAAC,CAAiB,EAAE,CAAiB;IAClE,IAAI,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,KAAK;QAAE,OAAO,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC;IAClD,IAAI,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,KAAK;QAAE,OAAO,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC;IAClD,OAAO,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC;AAC3B,CAAC;AAKD,SAAgB,cAAc,CAAC,OAAuB,EAAE,KAAa,EAAE,KAAa,EAAE,KAAK,GAAG,CAAC;IAC7F,MAAM,MAAM,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IACpD,OAAO,eAAe,CAAC,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;AAC/C,CAAC;AAKD,SAAgB,8BAA8B,CAAC,OAAuB;IACpE,MAAM,SAAS,GAAG,IAAI,GAAG,CAAS,mBAAmB,CAAC,IAAI,CAAC,CAAC;IAG5D,IAAI,cAAc,CAAC,OAAO,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC;QACtC,mBAAmB,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;IACnE,CAAC;IAGD,IAAI,cAAc,CAAC,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC;QACvC,mBAAmB,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;IACpE,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AASM,KAAK,UAAU,eAAe,CAAC,OAAe;IAEnD,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACzC,IAAI,MAAM,EAAE,CAAC;QACX,eAAM,CAAC,KAAK,CAAC,gCAAgC,OAAO,KAAK,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;QAC3E,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,IAAI,CAAC;QAEH,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,iBAAiB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QAC/E,MAAM,WAAW,GAAG,GAAG,YAAY,gBAAgB,CAAC;QAEpD,eAAM,CAAC,KAAK,CAAC,6BAA6B,WAAW,EAAE,CAAC,CAAC;QAEzD,MAAM,QAAQ,GAAG,MAAM,eAAK,CAAC,GAAG,CAAsB,WAAW,EAAE;YACjE,OAAO,EAAE,IAAI;YACb,cAAc,EAAE,CAAC,MAAc,EAAE,EAAE,CAAC,MAAM,GAAG,GAAG;SACjD,CAAC,CAAC;QAEH,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC;YAE7C,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;YACpC,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,eAAM,CAAC,IAAI,CAAC,8BAA8B,CAAC,CAAC;gBAC5C,OAAO,IAAI,CAAC;YACd,CAAC;YAGD,MAAM,aAAa,GAAG,QAAQ,CAAC,UAAU,IAAI,QAAQ,CAAC,UAAU,CAAC;YAEjE,IAAI,aAAa,EAAE,CAAC;gBAClB,MAAM,WAAW,GAAG,YAAY,CAAC,aAAa,CAAC,CAAC;gBAChD,IAAI,WAAW,EAAE,CAAC;oBAEhB,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;oBACvC,eAAM,CAAC,KAAK,CAAC,yBAAyB,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC;oBAC7D,OAAO,WAAW,CAAC;gBACrB,CAAC;YACH,CAAC;QACH,CAAC;QAED,eAAM,CAAC,IAAI,CAAC,wCAAwC,WAAW,EAAE,CAAC,CAAC;QACnE,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,eAAM,CAAC,IAAI,CAAC,gCAAgC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,EAAE,CAAC,CAAC;QACxG,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAKD,SAAgB,iBAAiB;IAC/B,YAAY,CAAC,KAAK,EAAE,CAAC;AACvB,CAAC;AAKD,SAAgB,gBAAgB,CAAC,OAAe;IAC9C,OAAO,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;AAC3C,CAAC;AAKD,SAAgB,gBAAgB,CAAC,OAAe,EAAE,OAAuB;IACvE,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;AACrC,CAAC;AAYD,SAAgB,uBAAuB,CACrC,QAA6C,EAC7C,OAA8B;IAE9B,IAAI,CAAC,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC9C,OAAO,EAAE,CAAC;IACZ,CAAC;IAGD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,MAAM,mBAAmB,GAAG,8BAA8B,CAAC,OAAO,CAAC,CAAC;IAEpE,MAAM,OAAO,GAA4B,EAAE,CAAC;IAC5C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QACpD,IAAI,mBAAmB,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACjC,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QACvB,CAAC;aAAM,CAAC;YACN,eAAM,CAAC,KAAK,CAAC,+CAA+C,GAAG,SAAS,OAAO,CAAC,OAAO,GAAG,CAAC,CAAC;QAC9F,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAGY,QAAA,kBAAkB,GAAG;IAChC,eAAe,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE;IAClD,aAAa,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE;CAClD,CAAC"}

View File

@@ -0,0 +1,70 @@
import { OperationInfo, ApiMethodMapping, CodeExample, TemplateInfo, RelatedResource } from '../utils/enhanced-documentation-fetcher';
interface NodeInfo {
nodeType: string;
name: string;
displayName: string;
description: string;
category?: string;
subcategory?: string;
icon?: string;
sourceCode: string;
credentialCode?: string;
documentationMarkdown?: string;
documentationUrl?: string;
documentationTitle?: string;
operations?: OperationInfo[];
apiMethods?: ApiMethodMapping[];
documentationExamples?: CodeExample[];
templates?: TemplateInfo[];
relatedResources?: RelatedResource[];
requiredScopes?: string[];
exampleWorkflow?: any;
exampleParameters?: any;
propertiesSchema?: any;
packageName: string;
version?: string;
codexData?: any;
aliases?: string[];
hasCredentials: boolean;
isTrigger: boolean;
isWebhook: boolean;
}
interface SearchOptions {
query?: string;
nodeType?: string;
packageName?: string;
category?: string;
hasCredentials?: boolean;
isTrigger?: boolean;
limit?: number;
}
export declare class NodeDocumentationService {
private db;
private extractor;
private docsFetcher;
private dbPath;
private initialized;
constructor(dbPath?: string);
private findDatabasePath;
private initializeAsync;
private ensureInitialized;
private initializeDatabase;
storeNode(nodeInfo: NodeInfo): Promise<void>;
getNodeInfo(nodeType: string): Promise<NodeInfo | null>;
searchNodes(options: SearchOptions): Promise<NodeInfo[]>;
listNodes(): Promise<NodeInfo[]>;
rebuildDatabase(): Promise<{
total: number;
successful: number;
failed: number;
errors: string[];
}>;
private parseNodeDefinition;
private rowToNodeInfo;
private generateHash;
private storeStatistics;
getStatistics(): Promise<any>;
close(): Promise<void>;
}
export {};
//# sourceMappingURL=node-documentation-service.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"node-documentation-service.d.ts","sourceRoot":"","sources":["../../src/services/node-documentation-service.ts"],"names":[],"mappings":"AAKA,OAAO,EAGL,aAAa,EACb,gBAAgB,EAChB,WAAW,EACX,YAAY,EACZ,eAAe,EAChB,MAAM,yCAAyC,CAAC;AAIjD,UAAU,QAAQ;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,UAAU,CAAC,EAAE,aAAa,EAAE,CAAC;IAC7B,UAAU,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAChC,qBAAqB,CAAC,EAAE,WAAW,EAAE,CAAC;IACtC,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;IAC3B,gBAAgB,CAAC,EAAE,eAAe,EAAE,CAAC;IACrC,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,eAAe,CAAC,EAAE,GAAG,CAAC;IACtB,iBAAiB,CAAC,EAAE,GAAG,CAAC;IACxB,gBAAgB,CAAC,EAAE,GAAG,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,GAAG,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,cAAc,EAAE,OAAO,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,UAAU,aAAa;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,wBAAwB;IACnC,OAAO,CAAC,EAAE,CAAgC;IAC1C,OAAO,CAAC,SAAS,CAAsB;IACvC,OAAO,CAAC,WAAW,CAA+B;IAClD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,WAAW,CAAgB;gBAEvB,MAAM,CAAC,EAAE,MAAM;IAiB3B,OAAO,CAAC,gBAAgB;YA0BV,eAAe;YAcf,iBAAiB;IAO/B,OAAO,CAAC,kBAAkB;IAwHpB,SAAS,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IA6D5C,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAevD,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAkDxD,SAAS,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;IAUhC,eAAe,IAAI,OAAO,CAAC;QAC/B,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,EAAE,CAAC;KAClB,CAAC;IAkHF,OAAO,CAAC,mBAAmB;IAwF3B,OAAO,CAAC,aAAa;IAoCrB,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,eAAe;IAgCjB,aAAa,IAAI,OAAO,CAAC,GAAG,CAAC;IAsC7B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAI7B"}

View File

@@ -0,0 +1,518 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.NodeDocumentationService = void 0;
const crypto_1 = require("crypto");
const path_1 = __importDefault(require("path"));
const logger_1 = require("../utils/logger");
const node_source_extractor_1 = require("../utils/node-source-extractor");
const enhanced_documentation_fetcher_1 = require("../utils/enhanced-documentation-fetcher");
const example_generator_1 = require("../utils/example-generator");
const database_adapter_1 = require("../database/database-adapter");
class NodeDocumentationService {
constructor(dbPath) {
this.db = null;
this.dbPath = dbPath || process.env.NODE_DB_PATH || this.findDatabasePath();
const dbDir = path_1.default.dirname(this.dbPath);
if (!require('fs').existsSync(dbDir)) {
require('fs').mkdirSync(dbDir, { recursive: true });
}
this.extractor = new node_source_extractor_1.NodeSourceExtractor();
this.docsFetcher = new enhanced_documentation_fetcher_1.EnhancedDocumentationFetcher();
this.initialized = this.initializeAsync();
}
findDatabasePath() {
const fs = require('fs');
const localPath = path_1.default.join(process.cwd(), 'data', 'nodes.db');
if (fs.existsSync(localPath)) {
return localPath;
}
const packagePath = path_1.default.join(__dirname, '..', '..', 'data', 'nodes.db');
if (fs.existsSync(packagePath)) {
return packagePath;
}
const globalPath = path_1.default.join(__dirname, '..', '..', '..', 'data', 'nodes.db');
if (fs.existsSync(globalPath)) {
return globalPath;
}
return localPath;
}
async initializeAsync() {
try {
this.db = await (0, database_adapter_1.createDatabaseAdapter)(this.dbPath);
this.initializeDatabase();
logger_1.logger.info('Node Documentation Service initialized');
}
catch (error) {
logger_1.logger.error('Failed to initialize database adapter', error);
throw error;
}
}
async ensureInitialized() {
await this.initialized;
if (!this.db) {
throw new Error('Database not initialized');
}
}
initializeDatabase() {
if (!this.db)
throw new Error('Database not initialized');
const schema = `
-- Main nodes table with documentation and examples
CREATE TABLE IF NOT EXISTS nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_type TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
display_name TEXT,
description TEXT,
category TEXT,
subcategory TEXT,
icon TEXT,
-- Source code
source_code TEXT NOT NULL,
credential_code TEXT,
code_hash TEXT NOT NULL,
code_length INTEGER NOT NULL,
-- Documentation
documentation_markdown TEXT,
documentation_url TEXT,
documentation_title TEXT,
-- Enhanced documentation fields (stored as JSON)
operations TEXT,
api_methods TEXT,
documentation_examples TEXT,
templates TEXT,
related_resources TEXT,
required_scopes TEXT,
-- Example usage
example_workflow TEXT,
example_parameters TEXT,
properties_schema TEXT,
-- Metadata
package_name TEXT NOT NULL,
version TEXT,
codex_data TEXT,
aliases TEXT,
-- Flags
has_credentials INTEGER DEFAULT 0,
is_trigger INTEGER DEFAULT 0,
is_webhook INTEGER DEFAULT 0,
-- Timestamps
extracted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Indexes
CREATE INDEX IF NOT EXISTS idx_nodes_package_name ON nodes(package_name);
CREATE INDEX IF NOT EXISTS idx_nodes_category ON nodes(category);
CREATE INDEX IF NOT EXISTS idx_nodes_code_hash ON nodes(code_hash);
CREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name);
CREATE INDEX IF NOT EXISTS idx_nodes_is_trigger ON nodes(is_trigger);
-- Full Text Search
CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
node_type,
name,
display_name,
description,
category,
documentation_markdown,
aliases,
content=nodes,
content_rowid=id
);
-- Triggers for FTS
CREATE TRIGGER IF NOT EXISTS nodes_ai AFTER INSERT ON nodes
BEGIN
INSERT INTO nodes_fts(rowid, node_type, name, display_name, description, category, documentation_markdown, aliases)
VALUES (new.id, new.node_type, new.name, new.display_name, new.description, new.category, new.documentation_markdown, new.aliases);
END;
CREATE TRIGGER IF NOT EXISTS nodes_ad AFTER DELETE ON nodes
BEGIN
DELETE FROM nodes_fts WHERE rowid = old.id;
END;
CREATE TRIGGER IF NOT EXISTS nodes_au AFTER UPDATE ON nodes
BEGIN
DELETE FROM nodes_fts WHERE rowid = old.id;
INSERT INTO nodes_fts(rowid, node_type, name, display_name, description, category, documentation_markdown, aliases)
VALUES (new.id, new.node_type, new.name, new.display_name, new.description, new.category, new.documentation_markdown, new.aliases);
END;
-- Documentation sources table
CREATE TABLE IF NOT EXISTS documentation_sources (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source TEXT NOT NULL,
commit_hash TEXT,
fetched_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Statistics table
CREATE TABLE IF NOT EXISTS extraction_stats (
id INTEGER PRIMARY KEY AUTOINCREMENT,
total_nodes INTEGER NOT NULL,
nodes_with_docs INTEGER NOT NULL,
nodes_with_examples INTEGER NOT NULL,
total_code_size INTEGER NOT NULL,
total_docs_size INTEGER NOT NULL,
extraction_date DATETIME DEFAULT CURRENT_TIMESTAMP
);
`;
this.db.exec(schema);
}
async storeNode(nodeInfo) {
await this.ensureInitialized();
const hash = this.generateHash(nodeInfo.sourceCode);
const stmt = this.db.prepare(`
INSERT OR REPLACE INTO nodes (
node_type, name, display_name, description, category, subcategory, icon,
source_code, credential_code, code_hash, code_length,
documentation_markdown, documentation_url, documentation_title,
operations, api_methods, documentation_examples, templates, related_resources, required_scopes,
example_workflow, example_parameters, properties_schema,
package_name, version, codex_data, aliases,
has_credentials, is_trigger, is_webhook
) VALUES (
@nodeType, @name, @displayName, @description, @category, @subcategory, @icon,
@sourceCode, @credentialCode, @hash, @codeLength,
@documentation, @documentationUrl, @documentationTitle,
@operations, @apiMethods, @documentationExamples, @templates, @relatedResources, @requiredScopes,
@exampleWorkflow, @exampleParameters, @propertiesSchema,
@packageName, @version, @codexData, @aliases,
@hasCredentials, @isTrigger, @isWebhook
)
`);
stmt.run({
nodeType: nodeInfo.nodeType,
name: nodeInfo.name,
displayName: nodeInfo.displayName || nodeInfo.name,
description: nodeInfo.description || '',
category: nodeInfo.category || 'Other',
subcategory: nodeInfo.subcategory || null,
icon: nodeInfo.icon || null,
sourceCode: nodeInfo.sourceCode,
credentialCode: nodeInfo.credentialCode || null,
hash,
codeLength: nodeInfo.sourceCode.length,
documentation: nodeInfo.documentationMarkdown || null,
documentationUrl: nodeInfo.documentationUrl || null,
documentationTitle: nodeInfo.documentationTitle || null,
operations: nodeInfo.operations ? JSON.stringify(nodeInfo.operations) : null,
apiMethods: nodeInfo.apiMethods ? JSON.stringify(nodeInfo.apiMethods) : null,
documentationExamples: nodeInfo.documentationExamples ? JSON.stringify(nodeInfo.documentationExamples) : null,
templates: nodeInfo.templates ? JSON.stringify(nodeInfo.templates) : null,
relatedResources: nodeInfo.relatedResources ? JSON.stringify(nodeInfo.relatedResources) : null,
requiredScopes: nodeInfo.requiredScopes ? JSON.stringify(nodeInfo.requiredScopes) : null,
exampleWorkflow: nodeInfo.exampleWorkflow ? JSON.stringify(nodeInfo.exampleWorkflow) : null,
exampleParameters: nodeInfo.exampleParameters ? JSON.stringify(nodeInfo.exampleParameters) : null,
propertiesSchema: nodeInfo.propertiesSchema ? JSON.stringify(nodeInfo.propertiesSchema) : null,
packageName: nodeInfo.packageName,
version: nodeInfo.version || null,
codexData: nodeInfo.codexData ? JSON.stringify(nodeInfo.codexData) : null,
aliases: nodeInfo.aliases ? JSON.stringify(nodeInfo.aliases) : null,
hasCredentials: nodeInfo.hasCredentials ? 1 : 0,
isTrigger: nodeInfo.isTrigger ? 1 : 0,
isWebhook: nodeInfo.isWebhook ? 1 : 0
});
}
async getNodeInfo(nodeType) {
await this.ensureInitialized();
const stmt = this.db.prepare(`
SELECT * FROM nodes WHERE node_type = ? OR name = ? COLLATE NOCASE
`);
const row = stmt.get(nodeType, nodeType);
if (!row)
return null;
return this.rowToNodeInfo(row);
}
async searchNodes(options) {
await this.ensureInitialized();
let query = 'SELECT * FROM nodes WHERE 1=1';
const params = {};
if (options.query) {
query += ` AND id IN (
SELECT rowid FROM nodes_fts
WHERE nodes_fts MATCH @query
)`;
params.query = options.query;
}
if (options.nodeType) {
query += ' AND node_type LIKE @nodeType';
params.nodeType = `%${options.nodeType}%`;
}
if (options.packageName) {
query += ' AND package_name = @packageName';
params.packageName = options.packageName;
}
if (options.category) {
query += ' AND category = @category';
params.category = options.category;
}
if (options.hasCredentials !== undefined) {
query += ' AND has_credentials = @hasCredentials';
params.hasCredentials = options.hasCredentials ? 1 : 0;
}
if (options.isTrigger !== undefined) {
query += ' AND is_trigger = @isTrigger';
params.isTrigger = options.isTrigger ? 1 : 0;
}
query += ' ORDER BY name LIMIT @limit';
params.limit = options.limit || 20;
const stmt = this.db.prepare(query);
const rows = stmt.all(params);
return rows.map(row => this.rowToNodeInfo(row));
}
async listNodes() {
await this.ensureInitialized();
const stmt = this.db.prepare('SELECT * FROM nodes ORDER BY name');
const rows = stmt.all();
return rows.map(row => this.rowToNodeInfo(row));
}
async rebuildDatabase() {
await this.ensureInitialized();
logger_1.logger.info('Starting complete database rebuild...');
this.db.exec('DELETE FROM nodes');
this.db.exec('DELETE FROM extraction_stats');
await this.docsFetcher.ensureDocsRepository();
const stats = {
total: 0,
successful: 0,
failed: 0,
errors: []
};
try {
const availableNodes = await this.extractor.listAvailableNodes();
stats.total = availableNodes.length;
logger_1.logger.info(`Found ${stats.total} nodes to process`);
const batchSize = 10;
for (let i = 0; i < availableNodes.length; i += batchSize) {
const batch = availableNodes.slice(i, i + batchSize);
await Promise.all(batch.map(async (node) => {
try {
const nodeType = `n8n-nodes-base.${node.name}`;
const nodeData = await this.extractor.extractNodeSource(nodeType);
if (!nodeData || !nodeData.sourceCode) {
throw new Error('Failed to extract node source');
}
const nodeDefinition = this.parseNodeDefinition(nodeData.sourceCode);
const enhancedDocs = await this.docsFetcher.getEnhancedNodeDocumentation(nodeType);
const example = example_generator_1.ExampleGenerator.generateFromNodeDefinition(nodeDefinition);
const nodeInfo = {
nodeType: nodeType,
name: node.name,
displayName: nodeDefinition.displayName || node.displayName || node.name,
description: nodeDefinition.description || node.description || '',
category: nodeDefinition.category || 'Other',
subcategory: nodeDefinition.subcategory,
icon: nodeDefinition.icon,
sourceCode: nodeData.sourceCode,
credentialCode: nodeData.credentialCode,
documentationMarkdown: enhancedDocs?.markdown,
documentationUrl: enhancedDocs?.url,
documentationTitle: enhancedDocs?.title,
operations: enhancedDocs?.operations,
apiMethods: enhancedDocs?.apiMethods,
documentationExamples: enhancedDocs?.examples,
templates: enhancedDocs?.templates,
relatedResources: enhancedDocs?.relatedResources,
requiredScopes: enhancedDocs?.requiredScopes,
exampleWorkflow: example,
exampleParameters: example.nodes[0]?.parameters,
propertiesSchema: nodeDefinition.properties,
packageName: nodeData.packageInfo?.name || 'n8n-nodes-base',
version: nodeDefinition.version,
codexData: nodeDefinition.codex,
aliases: nodeDefinition.alias,
hasCredentials: !!nodeData.credentialCode,
isTrigger: node.name.toLowerCase().includes('trigger'),
isWebhook: node.name.toLowerCase().includes('webhook')
};
await this.storeNode(nodeInfo);
stats.successful++;
logger_1.logger.debug(`Processed node: ${nodeType}`);
}
catch (error) {
stats.failed++;
const errorMsg = `Failed to process ${node.name}: ${error instanceof Error ? error.message : String(error)}`;
stats.errors.push(errorMsg);
logger_1.logger.error(errorMsg);
}
}));
logger_1.logger.info(`Progress: ${Math.min(i + batchSize, availableNodes.length)}/${stats.total} nodes processed`);
}
this.storeStatistics(stats);
logger_1.logger.info(`Database rebuild complete: ${stats.successful} successful, ${stats.failed} failed`);
}
catch (error) {
logger_1.logger.error('Database rebuild failed:', error);
throw error;
}
return stats;
}
parseNodeDefinition(sourceCode) {
const result = {
displayName: '',
description: '',
properties: [],
category: null,
subcategory: null,
icon: null,
version: null,
codex: null,
alias: null
};
try {
const displayNameMatch = sourceCode.match(/displayName\s*[:=]\s*['"`]([^'"`]+)['"`]/);
if (displayNameMatch) {
result.displayName = displayNameMatch[1];
}
const descriptionMatch = sourceCode.match(/description\s*[:=]\s*['"`]([^'"`]+)['"`]/);
if (descriptionMatch) {
result.description = descriptionMatch[1];
}
const iconMatch = sourceCode.match(/icon\s*[:=]\s*['"`]([^'"`]+)['"`]/);
if (iconMatch) {
result.icon = iconMatch[1];
}
const groupMatch = sourceCode.match(/group\s*[:=]\s*\[['"`]([^'"`]+)['"`]\]/);
if (groupMatch) {
result.category = groupMatch[1];
}
const versionMatch = sourceCode.match(/version\s*[:=]\s*(\d+)/);
if (versionMatch) {
result.version = parseInt(versionMatch[1]);
}
const subtitleMatch = sourceCode.match(/subtitle\s*[:=]\s*['"`]([^'"`]+)['"`]/);
if (subtitleMatch) {
result.subtitle = subtitleMatch[1];
}
const propsMatch = sourceCode.match(/properties\s*[:=]\s*(\[[\s\S]*?\])\s*[,}]/);
if (propsMatch) {
try {
result.properties = [];
}
catch (e) {
}
}
if (sourceCode.includes('implements.*ITrigger') ||
sourceCode.includes('polling:.*true') ||
sourceCode.includes('webhook:.*true') ||
result.displayName.toLowerCase().includes('trigger')) {
result.isTrigger = true;
}
if (sourceCode.includes('webhooks:') ||
sourceCode.includes('webhook:.*true') ||
result.displayName.toLowerCase().includes('webhook')) {
result.isWebhook = true;
}
}
catch (error) {
logger_1.logger.debug('Error parsing node definition:', error);
}
return result;
}
rowToNodeInfo(row) {
return {
nodeType: row.node_type,
name: row.name,
displayName: row.display_name,
description: row.description,
category: row.category,
subcategory: row.subcategory,
icon: row.icon,
sourceCode: row.source_code,
credentialCode: row.credential_code,
documentationMarkdown: row.documentation_markdown,
documentationUrl: row.documentation_url,
documentationTitle: row.documentation_title,
operations: row.operations ? JSON.parse(row.operations) : null,
apiMethods: row.api_methods ? JSON.parse(row.api_methods) : null,
documentationExamples: row.documentation_examples ? JSON.parse(row.documentation_examples) : null,
templates: row.templates ? JSON.parse(row.templates) : null,
relatedResources: row.related_resources ? JSON.parse(row.related_resources) : null,
requiredScopes: row.required_scopes ? JSON.parse(row.required_scopes) : null,
exampleWorkflow: row.example_workflow ? JSON.parse(row.example_workflow) : null,
exampleParameters: row.example_parameters ? JSON.parse(row.example_parameters) : null,
propertiesSchema: row.properties_schema ? JSON.parse(row.properties_schema) : null,
packageName: row.package_name,
version: row.version,
codexData: row.codex_data ? JSON.parse(row.codex_data) : null,
aliases: row.aliases ? JSON.parse(row.aliases) : null,
hasCredentials: row.has_credentials === 1,
isTrigger: row.is_trigger === 1,
isWebhook: row.is_webhook === 1
};
}
generateHash(content) {
return (0, crypto_1.createHash)('sha256').update(content).digest('hex');
}
storeStatistics(stats) {
if (!this.db)
throw new Error('Database not initialized');
const stmt = this.db.prepare(`
INSERT INTO extraction_stats (
total_nodes, nodes_with_docs, nodes_with_examples,
total_code_size, total_docs_size
) VALUES (?, ?, ?, ?, ?)
`);
const sizeStats = this.db.prepare(`
SELECT
COUNT(*) as total,
SUM(CASE WHEN documentation_markdown IS NOT NULL THEN 1 ELSE 0 END) as with_docs,
SUM(CASE WHEN example_workflow IS NOT NULL THEN 1 ELSE 0 END) as with_examples,
SUM(code_length) as code_size,
SUM(LENGTH(documentation_markdown)) as docs_size
FROM nodes
`).get();
stmt.run(stats.successful, sizeStats?.with_docs || 0, sizeStats?.with_examples || 0, sizeStats?.code_size || 0, sizeStats?.docs_size || 0);
}
async getStatistics() {
await this.ensureInitialized();
const stats = this.db.prepare(`
SELECT
COUNT(*) as totalNodes,
COUNT(DISTINCT package_name) as totalPackages,
SUM(code_length) as totalCodeSize,
SUM(CASE WHEN documentation_markdown IS NOT NULL THEN 1 ELSE 0 END) as nodesWithDocs,
SUM(CASE WHEN example_workflow IS NOT NULL THEN 1 ELSE 0 END) as nodesWithExamples,
SUM(has_credentials) as nodesWithCredentials,
SUM(is_trigger) as triggerNodes,
SUM(is_webhook) as webhookNodes
FROM nodes
`).get();
const packages = this.db.prepare(`
SELECT package_name as package, COUNT(*) as count
FROM nodes
GROUP BY package_name
ORDER BY count DESC
`).all();
return {
totalNodes: stats?.totalNodes || 0,
totalPackages: stats?.totalPackages || 0,
totalCodeSize: stats?.totalCodeSize || 0,
nodesWithDocs: stats?.nodesWithDocs || 0,
nodesWithExamples: stats?.nodesWithExamples || 0,
nodesWithCredentials: stats?.nodesWithCredentials || 0,
triggerNodes: stats?.triggerNodes || 0,
webhookNodes: stats?.webhookNodes || 0,
packageDistribution: packages
};
}
async close() {
await this.ensureInitialized();
this.db.close();
}
}
exports.NodeDocumentationService = NodeDocumentationService;
//# sourceMappingURL=node-documentation-service.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,44 @@
import { BreakingChangeDetector } from './breaking-change-detector';
import { NodeVersionService } from './node-version-service';
export interface MigrationResult {
success: boolean;
nodeId: string;
nodeName: string;
fromVersion: string;
toVersion: string;
appliedMigrations: AppliedMigration[];
remainingIssues: string[];
confidence: 'HIGH' | 'MEDIUM' | 'LOW';
updatedNode: any;
}
export interface AppliedMigration {
propertyName: string;
action: string;
oldValue?: any;
newValue?: any;
description: string;
}
export declare class NodeMigrationService {
private versionService;
private breakingChangeDetector;
constructor(versionService: NodeVersionService, breakingChangeDetector: BreakingChangeDetector);
migrateNode(node: any, fromVersion: string, toVersion: string): Promise<MigrationResult>;
private applyMigration;
private addProperty;
private removeProperty;
private renameProperty;
private setDefault;
private resolveDefaultValue;
private parseVersion;
validateMigratedNode(node: any, nodeType: string): Promise<{
valid: boolean;
errors: string[];
warnings: string[];
}>;
migrateWorkflowNodes(workflow: any, targetVersions: Record<string, string>): Promise<{
success: boolean;
results: MigrationResult[];
overallConfidence: 'HIGH' | 'MEDIUM' | 'LOW';
}>;
}
//# sourceMappingURL=node-migration-service.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"node-migration-service.d.ts","sourceRoot":"","sources":["../../src/services/node-migration-service.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,sBAAsB,EAAkB,MAAM,4BAA4B,CAAC;AACpF,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAE5D,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,gBAAgB,EAAE,CAAC;IACtC,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IACtC,WAAW,EAAE,GAAG,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,GAAG,CAAC;IACf,QAAQ,CAAC,EAAE,GAAG,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,oBAAoB;IAE7B,OAAO,CAAC,cAAc;IACtB,OAAO,CAAC,sBAAsB;gBADtB,cAAc,EAAE,kBAAkB,EAClC,sBAAsB,EAAE,sBAAsB;IAMlD,WAAW,CACf,IAAI,EAAE,GAAG,EACT,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC;IA4D3B,OAAO,CAAC,cAAc;IA0BtB,OAAO,CAAC,WAAW;IAkCnB,OAAO,CAAC,cAAc;IAkCtB,OAAO,CAAC,cAAc;IAiDtB,OAAO,CAAC,UAAU;IAqClB,OAAO,CAAC,mBAAmB;IAoB3B,OAAO,CAAC,YAAY;IAcd,oBAAoB,CAAC,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAC/D,KAAK,EAAE,OAAO,CAAC;QACf,MAAM,EAAE,MAAM,EAAE,CAAC;QACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;IAuCI,oBAAoB,CACxB,QAAQ,EAAE,GAAG,EACb,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACrC,OAAO,CAAC;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,EAAE,eAAe,EAAE,CAAC;QAC3B,iBAAiB,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;KAC9C,CAAC;CAmCH"}

231
dist/services/node-migration-service.js vendored Normal file
View File

@@ -0,0 +1,231 @@
"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

File diff suppressed because one or more lines are too long

5
dist/services/node-sanitizer.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
import { WorkflowNode } from '../types/n8n-api';
export declare function sanitizeNode(node: WorkflowNode): WorkflowNode;
export declare function sanitizeWorkflowNodes(workflow: any): any;
export declare function validateNodeMetadata(node: WorkflowNode): string[];
//# sourceMappingURL=node-sanitizer.d.ts.map

1
dist/services/node-sanitizer.d.ts.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"node-sanitizer.d.ts","sourceRoot":"","sources":["../../src/services/node-sanitizer.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAKhD,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,YAAY,CAa7D;AAKD,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,GAAG,GAAG,GAAG,CASxD;AAoND,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,EAAE,CAgEjE"}

225
dist/services/node-sanitizer.js vendored Normal file
View File

@@ -0,0 +1,225 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.sanitizeNode = sanitizeNode;
exports.sanitizeWorkflowNodes = sanitizeWorkflowNodes;
exports.validateNodeMetadata = validateNodeMetadata;
const logger_1 = require("../utils/logger");
function sanitizeNode(node) {
const sanitized = { ...node };
if (isFilterBasedNode(node.type, node.typeVersion)) {
sanitized.parameters = sanitizeFilterBasedNode(sanitized.parameters, node.type, node.typeVersion);
}
return sanitized;
}
function sanitizeWorkflowNodes(workflow) {
if (!workflow.nodes || !Array.isArray(workflow.nodes)) {
return workflow;
}
return {
...workflow,
nodes: workflow.nodes.map((node) => sanitizeNode(node))
};
}
function isFilterBasedNode(nodeType, typeVersion) {
if (nodeType === 'n8n-nodes-base.if') {
return typeVersion >= 2.2;
}
if (nodeType === 'n8n-nodes-base.switch') {
return typeVersion >= 3.2;
}
return false;
}
function sanitizeFilterBasedNode(parameters, nodeType, typeVersion) {
const sanitized = { ...parameters };
if (nodeType === 'n8n-nodes-base.if' && typeVersion >= 2.2) {
sanitized.conditions = sanitizeFilterConditions(sanitized.conditions);
}
if (nodeType === 'n8n-nodes-base.switch' && typeVersion >= 3.2) {
if (sanitized.rules && typeof sanitized.rules === 'object') {
const rules = sanitized.rules;
if (rules.rules && Array.isArray(rules.rules)) {
rules.rules = rules.rules.map((rule) => ({
...rule,
conditions: sanitizeFilterConditions(rule.conditions)
}));
}
}
}
return sanitized;
}
function sanitizeFilterConditions(conditions) {
if (!conditions || typeof conditions !== 'object') {
return conditions;
}
const sanitized = { ...conditions };
if (!sanitized.options) {
sanitized.options = {};
}
const requiredOptions = {
version: 2,
leftValue: '',
caseSensitive: true,
typeValidation: 'strict'
};
sanitized.options = {
...requiredOptions,
...sanitized.options
};
if (sanitized.conditions && Array.isArray(sanitized.conditions)) {
sanitized.conditions = sanitized.conditions.map((condition) => sanitizeCondition(condition));
}
return sanitized;
}
function sanitizeCondition(condition) {
if (!condition || typeof condition !== 'object') {
return condition;
}
const sanitized = { ...condition };
if (!sanitized.id) {
sanitized.id = generateConditionId();
}
if (sanitized.operator) {
sanitized.operator = sanitizeOperator(sanitized.operator);
}
return sanitized;
}
function sanitizeOperator(operator) {
if (!operator || typeof operator !== 'object') {
return operator;
}
const sanitized = { ...operator };
if (sanitized.type && !sanitized.operation) {
const typeValue = sanitized.type;
if (isOperationName(typeValue)) {
logger_1.logger.debug(`Fixing operator structure: converting type="${typeValue}" to operation`);
const dataType = inferDataType(typeValue);
sanitized.type = dataType;
sanitized.operation = typeValue;
}
}
if (sanitized.operation) {
if (isUnaryOperator(sanitized.operation)) {
sanitized.singleValue = true;
}
else {
delete sanitized.singleValue;
}
}
return sanitized;
}
function isOperationName(value) {
const dataTypes = ['string', 'number', 'boolean', 'dateTime', 'array', 'object'];
return !dataTypes.includes(value) && /^[a-z][a-zA-Z]*$/.test(value);
}
function inferDataType(operation) {
const booleanOps = ['true', 'false', 'isEmpty', 'isNotEmpty'];
if (booleanOps.includes(operation)) {
return 'boolean';
}
const numberOps = ['isNumeric', 'gt', 'gte', 'lt', 'lte'];
if (numberOps.some(op => operation.includes(op))) {
return 'number';
}
const dateOps = ['after', 'before', 'afterDate', 'beforeDate'];
if (dateOps.some(op => operation.includes(op))) {
return 'dateTime';
}
return 'string';
}
function isUnaryOperator(operation) {
const unaryOps = [
'isEmpty',
'isNotEmpty',
'true',
'false',
'isNumeric'
];
return unaryOps.includes(operation);
}
function generateConditionId() {
return `condition-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
function validateNodeMetadata(node) {
const issues = [];
if (!isFilterBasedNode(node.type, node.typeVersion)) {
return issues;
}
if (node.type === 'n8n-nodes-base.if') {
const conditions = node.parameters.conditions;
if (!conditions?.options) {
issues.push('Missing conditions.options');
}
else {
const required = ['version', 'leftValue', 'typeValidation', 'caseSensitive'];
for (const field of required) {
if (!(field in conditions.options)) {
issues.push(`Missing conditions.options.${field}`);
}
}
}
if (conditions?.conditions && Array.isArray(conditions.conditions)) {
for (let i = 0; i < conditions.conditions.length; i++) {
const condition = conditions.conditions[i];
const operatorIssues = validateOperator(condition.operator, `conditions.conditions[${i}].operator`);
issues.push(...operatorIssues);
}
}
}
if (node.type === 'n8n-nodes-base.switch') {
const rules = node.parameters.rules;
if (rules?.rules && Array.isArray(rules.rules)) {
for (let i = 0; i < rules.rules.length; i++) {
const rule = rules.rules[i];
if (!rule.conditions?.options) {
issues.push(`Missing rules.rules[${i}].conditions.options`);
}
else {
const required = ['version', 'leftValue', 'typeValidation', 'caseSensitive'];
for (const field of required) {
if (!(field in rule.conditions.options)) {
issues.push(`Missing rules.rules[${i}].conditions.options.${field}`);
}
}
}
if (rule.conditions?.conditions && Array.isArray(rule.conditions.conditions)) {
for (let j = 0; j < rule.conditions.conditions.length; j++) {
const condition = rule.conditions.conditions[j];
const operatorIssues = validateOperator(condition.operator, `rules.rules[${i}].conditions.conditions[${j}].operator`);
issues.push(...operatorIssues);
}
}
}
}
}
return issues;
}
function validateOperator(operator, path) {
const issues = [];
if (!operator || typeof operator !== 'object') {
issues.push(`${path}: operator is missing or not an object`);
return issues;
}
if (!operator.type) {
issues.push(`${path}: missing required field 'type'`);
}
else if (!['string', 'number', 'boolean', 'dateTime', 'array', 'object'].includes(operator.type)) {
issues.push(`${path}: invalid type "${operator.type}" (must be data type, not operation)`);
}
if (!operator.operation) {
issues.push(`${path}: missing required field 'operation'`);
}
if (operator.operation) {
if (isUnaryOperator(operator.operation)) {
if (operator.singleValue !== true) {
issues.push(`${path}: unary operator "${operator.operation}" requires singleValue: true`);
}
}
else {
if (operator.singleValue === true) {
issues.push(`${path}: binary operator "${operator.operation}" should not have singleValue: true (only unary operators need this)`);
}
}
}
return issues;
}
//# sourceMappingURL=node-sanitizer.js.map

1
dist/services/node-sanitizer.js.map vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,51 @@
import { NodeRepository } from '../database/node-repository';
export interface NodeSuggestion {
nodeType: string;
displayName: string;
confidence: number;
reason: string;
category?: string;
description?: string;
}
export interface SimilarityScore {
nameSimilarity: number;
categoryMatch: number;
packageMatch: number;
patternMatch: number;
totalScore: number;
}
export interface CommonMistakePattern {
pattern: string;
suggestion: string;
confidence: number;
reason: string;
}
export declare class NodeSimilarityService {
private static readonly SCORING_THRESHOLD;
private static readonly TYPO_EDIT_DISTANCE;
private static readonly SHORT_SEARCH_LENGTH;
private static readonly CACHE_DURATION_MS;
private static readonly AUTO_FIX_CONFIDENCE;
private repository;
private commonMistakes;
private nodeCache;
private cacheExpiry;
private cacheVersion;
constructor(repository: NodeRepository);
private initializeCommonMistakes;
private isCommonNodeWithoutPrefix;
findSimilarNodes(invalidType: string, limit?: number): Promise<NodeSuggestion[]>;
private checkCommonMistakes;
private calculateSimilarityScore;
private createSuggestion;
private normalizeNodeType;
private getStringSimilarity;
private getEditDistance;
private getCachedNodes;
invalidateCache(): void;
refreshCache(): Promise<void>;
formatSuggestionMessage(suggestions: NodeSuggestion[], invalidType: string): string;
isAutoFixable(suggestion: NodeSuggestion): boolean;
clearCache(): void;
}
//# sourceMappingURL=node-similarity-service.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"node-similarity-service.d.ts","sourceRoot":"","sources":["../../src/services/node-similarity-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAG7D,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,qBAAqB;IAEhC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAM;IAC/C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAK;IAC/C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAK;IAChD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAiB;IAC1D,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAO;IAElD,OAAO,CAAC,UAAU,CAAiB;IACnC,OAAO,CAAC,cAAc,CAAsC;IAC5D,OAAO,CAAC,SAAS,CAAsB;IACvC,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,YAAY,CAAa;gBAErB,UAAU,EAAE,cAAc;IAStC,OAAO,CAAC,wBAAwB;IAkDhC,OAAO,CAAC,yBAAyB;IAuB3B,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,GAAE,MAAU,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IA8CzF,OAAO,CAAC,mBAAmB;IA0E3B,OAAO,CAAC,wBAAwB;IAuEhC,OAAO,CAAC,gBAAgB;IA2BxB,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,mBAAmB;IAgB3B,OAAO,CAAC,eAAe;YAgDT,cAAc;IAqCrB,eAAe,IAAI,IAAI;IAUjB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ1C,uBAAuB,CAAC,WAAW,EAAE,cAAc,EAAE,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM;IA8BnF,aAAa,CAAC,UAAU,EAAE,cAAc,GAAG,OAAO;IAQlD,UAAU,IAAI,IAAI;CAGnB"}

335
dist/services/node-similarity-service.js vendored Normal file
View File

@@ -0,0 +1,335 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NodeSimilarityService = void 0;
const logger_1 = require("../utils/logger");
class NodeSimilarityService {
constructor(repository) {
this.nodeCache = null;
this.cacheExpiry = 0;
this.cacheVersion = 0;
this.repository = repository;
this.commonMistakes = this.initializeCommonMistakes();
}
initializeCommonMistakes() {
const patterns = new Map();
patterns.set('case_variations', [
{ pattern: 'httprequest', suggestion: 'nodes-base.httpRequest', confidence: 0.95, reason: 'Incorrect capitalization' },
{ pattern: 'webhook', suggestion: 'nodes-base.webhook', confidence: 0.95, reason: 'Incorrect capitalization' },
{ pattern: 'slack', suggestion: 'nodes-base.slack', confidence: 0.9, reason: 'Missing package prefix' },
{ pattern: 'gmail', suggestion: 'nodes-base.gmail', confidence: 0.9, reason: 'Missing package prefix' },
{ pattern: 'googlesheets', suggestion: 'nodes-base.googleSheets', confidence: 0.9, reason: 'Missing package prefix' },
{ pattern: 'telegram', suggestion: 'nodes-base.telegram', confidence: 0.9, reason: 'Missing package prefix' },
]);
patterns.set('specific_variations', [
{ pattern: 'HttpRequest', suggestion: 'nodes-base.httpRequest', confidence: 0.95, reason: 'Incorrect capitalization' },
{ pattern: 'HTTPRequest', suggestion: 'nodes-base.httpRequest', confidence: 0.95, reason: 'Common capitalization mistake' },
{ pattern: 'Webhook', suggestion: 'nodes-base.webhook', confidence: 0.95, reason: 'Incorrect capitalization' },
{ pattern: 'WebHook', suggestion: 'nodes-base.webhook', confidence: 0.95, reason: 'Common capitalization mistake' },
]);
patterns.set('deprecated_prefixes', [
{ pattern: 'n8n-nodes-base.', suggestion: 'nodes-base.', confidence: 0.95, reason: 'Full package name used instead of short form' },
{ pattern: '@n8n/n8n-nodes-langchain.', suggestion: 'nodes-langchain.', confidence: 0.95, reason: 'Full package name used instead of short form' },
]);
patterns.set('typos', [
{ pattern: 'htprequest', suggestion: 'nodes-base.httpRequest', confidence: 0.8, reason: 'Likely typo' },
{ pattern: 'httpreqest', suggestion: 'nodes-base.httpRequest', confidence: 0.8, reason: 'Likely typo' },
{ pattern: 'webook', suggestion: 'nodes-base.webhook', confidence: 0.8, reason: 'Likely typo' },
{ pattern: 'slak', suggestion: 'nodes-base.slack', confidence: 0.8, reason: 'Likely typo' },
{ pattern: 'googlesheets', suggestion: 'nodes-base.googleSheets', confidence: 0.8, reason: 'Likely typo' },
]);
patterns.set('ai_nodes', [
{ pattern: 'openai', suggestion: 'nodes-langchain.openAi', confidence: 0.85, reason: 'AI node - incorrect package' },
{ pattern: 'nodes-base.openai', suggestion: 'nodes-langchain.openAi', confidence: 0.9, reason: 'Wrong package - OpenAI is in LangChain package' },
{ pattern: 'chatopenai', suggestion: 'nodes-langchain.lmChatOpenAi', confidence: 0.85, reason: 'LangChain node naming convention' },
{ pattern: 'vectorstore', suggestion: 'nodes-langchain.vectorStoreInMemory', confidence: 0.7, reason: 'Generic vector store reference' },
]);
return patterns;
}
isCommonNodeWithoutPrefix(type) {
const commonNodes = {
'httprequest': 'nodes-base.httpRequest',
'webhook': 'nodes-base.webhook',
'slack': 'nodes-base.slack',
'gmail': 'nodes-base.gmail',
'googlesheets': 'nodes-base.googleSheets',
'telegram': 'nodes-base.telegram',
'discord': 'nodes-base.discord',
'notion': 'nodes-base.notion',
'airtable': 'nodes-base.airtable',
'postgres': 'nodes-base.postgres',
'mysql': 'nodes-base.mySql',
'mongodb': 'nodes-base.mongoDb',
};
const normalized = type.toLowerCase();
return commonNodes[normalized] || null;
}
async findSimilarNodes(invalidType, limit = 5) {
if (!invalidType || invalidType.trim() === '') {
return [];
}
const suggestions = [];
const mistakeSuggestion = this.checkCommonMistakes(invalidType);
if (mistakeSuggestion) {
suggestions.push(mistakeSuggestion);
}
const allNodes = await this.getCachedNodes();
const scores = allNodes.map(node => ({
node,
score: this.calculateSimilarityScore(invalidType, node)
}));
scores.sort((a, b) => b.score.totalScore - a.score.totalScore);
for (const { node, score } of scores) {
if (suggestions.some(s => s.nodeType === node.nodeType)) {
continue;
}
if (score.totalScore >= NodeSimilarityService.SCORING_THRESHOLD) {
suggestions.push(this.createSuggestion(node, score));
}
if (suggestions.length >= limit) {
break;
}
}
return suggestions;
}
checkCommonMistakes(invalidType) {
const cleanType = invalidType.trim();
const lowerType = cleanType.toLowerCase();
const commonNodeSuggestion = this.isCommonNodeWithoutPrefix(cleanType);
if (commonNodeSuggestion) {
const node = this.repository.getNode(commonNodeSuggestion);
if (node) {
return {
nodeType: commonNodeSuggestion,
displayName: node.displayName,
confidence: 0.9,
reason: 'Missing package prefix',
category: node.category,
description: node.description
};
}
}
for (const [category, patterns] of this.commonMistakes) {
if (category === 'deprecated_prefixes') {
for (const pattern of patterns) {
if (cleanType.startsWith(pattern.pattern)) {
const actualSuggestion = cleanType.replace(pattern.pattern, pattern.suggestion);
const node = this.repository.getNode(actualSuggestion);
if (node) {
return {
nodeType: actualSuggestion,
displayName: node.displayName,
confidence: pattern.confidence,
reason: pattern.reason,
category: node.category,
description: node.description
};
}
}
}
}
}
for (const [category, patterns] of this.commonMistakes) {
if (category === 'deprecated_prefixes')
continue;
for (const pattern of patterns) {
const match = category === 'specific_variations'
? cleanType === pattern.pattern
: lowerType === pattern.pattern.toLowerCase();
if (match && pattern.suggestion) {
const node = this.repository.getNode(pattern.suggestion);
if (node) {
return {
nodeType: pattern.suggestion,
displayName: node.displayName,
confidence: pattern.confidence,
reason: pattern.reason,
category: node.category,
description: node.description
};
}
}
}
}
return null;
}
calculateSimilarityScore(invalidType, node) {
const cleanInvalid = this.normalizeNodeType(invalidType);
const cleanValid = this.normalizeNodeType(node.nodeType);
const displayNameClean = this.normalizeNodeType(node.displayName);
const isShortSearch = invalidType.length <= NodeSimilarityService.SHORT_SEARCH_LENGTH;
let nameSimilarity = Math.max(this.getStringSimilarity(cleanInvalid, cleanValid), this.getStringSimilarity(cleanInvalid, displayNameClean)) * 40;
if (isShortSearch && (cleanValid.includes(cleanInvalid) || displayNameClean.includes(cleanInvalid))) {
nameSimilarity = Math.max(nameSimilarity, 10);
}
let categoryMatch = 0;
if (node.category) {
const categoryClean = this.normalizeNodeType(node.category);
if (cleanInvalid.includes(categoryClean) || categoryClean.includes(cleanInvalid)) {
categoryMatch = 20;
}
}
let packageMatch = 0;
const invalidParts = cleanInvalid.split(/[.-]/);
const validParts = cleanValid.split(/[.-]/);
if (invalidParts[0] === validParts[0]) {
packageMatch = 15;
}
let patternMatch = 0;
if (cleanValid.includes(cleanInvalid) || displayNameClean.includes(cleanInvalid)) {
patternMatch = isShortSearch ? 45 : 25;
}
else if (this.getEditDistance(cleanInvalid, cleanValid) <= NodeSimilarityService.TYPO_EDIT_DISTANCE) {
patternMatch = 20;
}
else if (this.getEditDistance(cleanInvalid, displayNameClean) <= NodeSimilarityService.TYPO_EDIT_DISTANCE) {
patternMatch = 18;
}
if (isShortSearch && (cleanValid.startsWith(cleanInvalid) || displayNameClean.startsWith(cleanInvalid))) {
patternMatch = Math.max(patternMatch, 40);
}
const totalScore = nameSimilarity + categoryMatch + packageMatch + patternMatch;
return {
nameSimilarity,
categoryMatch,
packageMatch,
patternMatch,
totalScore
};
}
createSuggestion(node, score) {
let reason = 'Similar node';
if (score.patternMatch >= 20) {
reason = 'Name similarity';
}
else if (score.categoryMatch >= 15) {
reason = 'Same category';
}
else if (score.packageMatch >= 10) {
reason = 'Same package';
}
const confidence = Math.min(score.totalScore / 100, 1);
return {
nodeType: node.nodeType,
displayName: node.displayName,
confidence,
reason,
category: node.category,
description: node.description
};
}
normalizeNodeType(type) {
return type
.toLowerCase()
.replace(/[^a-z0-9]/g, '')
.trim();
}
getStringSimilarity(s1, s2) {
if (s1 === s2)
return 1;
if (!s1 || !s2)
return 0;
const distance = this.getEditDistance(s1, s2);
const maxLen = Math.max(s1.length, s2.length);
return 1 - (distance / maxLen);
}
getEditDistance(s1, s2, maxDistance = 5) {
if (s1 === s2)
return 0;
const m = s1.length;
const n = s2.length;
const lengthDiff = Math.abs(m - n);
if (lengthDiff > maxDistance)
return maxDistance + 1;
if (m === 0)
return n;
if (n === 0)
return m;
let prev = Array(n + 1).fill(0).map((_, i) => i);
for (let i = 1; i <= m; i++) {
const curr = [i];
let minInRow = i;
for (let j = 1; j <= n; j++) {
const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
const val = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
curr.push(val);
minInRow = Math.min(minInRow, val);
}
if (minInRow > maxDistance) {
return maxDistance + 1;
}
prev = curr;
}
return prev[n];
}
async getCachedNodes() {
const now = Date.now();
if (!this.nodeCache || now > this.cacheExpiry) {
try {
const newNodes = this.repository.getAllNodes();
if (newNodes && newNodes.length > 0) {
this.nodeCache = newNodes;
this.cacheExpiry = now + NodeSimilarityService.CACHE_DURATION_MS;
this.cacheVersion++;
logger_1.logger.debug('Node cache refreshed', {
count: newNodes.length,
version: this.cacheVersion
});
}
else if (this.nodeCache) {
logger_1.logger.warn('Node fetch returned empty, using stale cache');
}
}
catch (error) {
logger_1.logger.error('Failed to fetch nodes for similarity service', error);
if (this.nodeCache) {
logger_1.logger.info('Using stale cache due to fetch error');
return this.nodeCache;
}
return [];
}
}
return this.nodeCache || [];
}
invalidateCache() {
this.nodeCache = null;
this.cacheExpiry = 0;
this.cacheVersion++;
logger_1.logger.debug('Node cache invalidated', { version: this.cacheVersion });
}
async refreshCache() {
this.invalidateCache();
await this.getCachedNodes();
}
formatSuggestionMessage(suggestions, invalidType) {
if (suggestions.length === 0) {
return `Unknown node type: "${invalidType}". No similar nodes found.`;
}
let message = `Unknown node type: "${invalidType}"\n\nDid you mean one of these?\n`;
for (const suggestion of suggestions) {
const confidence = Math.round(suggestion.confidence * 100);
message += `${suggestion.nodeType} (${confidence}% match)`;
if (suggestion.displayName) {
message += ` - ${suggestion.displayName}`;
}
message += `\n${suggestion.reason}`;
if (suggestion.confidence >= 0.9) {
message += ' (can be auto-fixed)';
}
message += '\n';
}
return message;
}
isAutoFixable(suggestion) {
return suggestion.confidence >= NodeSimilarityService.AUTO_FIX_CONFIDENCE;
}
clearCache() {
this.invalidateCache();
}
}
exports.NodeSimilarityService = NodeSimilarityService;
NodeSimilarityService.SCORING_THRESHOLD = 50;
NodeSimilarityService.TYPO_EDIT_DISTANCE = 2;
NodeSimilarityService.SHORT_SEARCH_LENGTH = 5;
NodeSimilarityService.CACHE_DURATION_MS = 5 * 60 * 1000;
NodeSimilarityService.AUTO_FIX_CONFIDENCE = 0.9;
//# sourceMappingURL=node-similarity-service.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,37 @@
import { ValidationError, ValidationWarning } from './config-validator';
export interface NodeValidationContext {
config: Record<string, any>;
errors: ValidationError[];
warnings: ValidationWarning[];
suggestions: string[];
autofix: Record<string, any>;
}
export declare class NodeSpecificValidators {
static validateSlack(context: NodeValidationContext): void;
private static validateSlackSendMessage;
private static validateSlackUpdateMessage;
private static validateSlackDeleteMessage;
private static validateSlackCreateChannel;
static validateGoogleSheets(context: NodeValidationContext): void;
private static validateGoogleSheetsAppend;
private static validateGoogleSheetsRead;
private static validateGoogleSheetsUpdate;
private static validateGoogleSheetsDelete;
private static validateGoogleSheetsRange;
static validateOpenAI(context: NodeValidationContext): void;
static validateMongoDB(context: NodeValidationContext): void;
static validatePostgres(context: NodeValidationContext): void;
static validateAIAgent(context: NodeValidationContext): void;
static validateMySQL(context: NodeValidationContext): void;
private static validateSQLQuery;
static validateHttpRequest(context: NodeValidationContext): void;
static validateWebhook(context: NodeValidationContext): void;
static validateCode(context: NodeValidationContext): void;
private static validateJavaScriptCode;
private static validatePythonCode;
private static validateReturnStatement;
private static validateN8nVariables;
private static validateCodeSecurity;
static validateSet(context: NodeValidationContext): void;
}
//# sourceMappingURL=node-specific-validators.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"node-specific-validators.d.ts","sourceRoot":"","sources":["../../src/services/node-specific-validators.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAExE,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC5B,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC9B;AAED,qBAAa,sBAAsB;IAIjC,MAAM,CAAC,aAAa,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAqE1D,OAAO,CAAC,MAAM,CAAC,wBAAwB;IAkDvC,OAAO,CAAC,MAAM,CAAC,0BAA0B;IAsBzC,OAAO,CAAC,MAAM,CAAC,0BAA0B;IA4BzC,OAAO,CAAC,MAAM,CAAC,0BAA0B;IA2CzC,MAAM,CAAC,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAiDjE,OAAO,CAAC,MAAM,CAAC,0BAA0B;IA0BzC,OAAO,CAAC,MAAM,CAAC,wBAAwB;IAkBvC,OAAO,CAAC,MAAM,CAAC,0BAA0B;IAsBzC,OAAO,CAAC,MAAM,CAAC,0BAA0B;IA4BzC,OAAO,CAAC,MAAM,CAAC,yBAAyB;IAwCxC,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAwF3D,MAAM,CAAC,eAAe,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAyG5D,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAkI7D,MAAM,CAAC,eAAe,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAmG5D,MAAM,CAAC,aAAa,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IA6F1D,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAkF/B,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAiGhE,MAAM,CAAC,eAAe,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IA6D5D,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IA8DzD,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAuDrC,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAuDjC,OAAO,CAAC,MAAM,CAAC,uBAAuB;IAsFtC,OAAO,CAAC,MAAM,CAAC,oBAAoB;IAuKnC,OAAO,CAAC,MAAM,CAAC,oBAAoB;IA2EnC,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;CAoDzD"}

1331
dist/services/node-specific-validators.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

63
dist/services/node-version-service.d.ts vendored Normal file
View File

@@ -0,0 +1,63 @@
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;
hasBreakingChanges: boolean;
recommendUpgrade: boolean;
confidence: 'HIGH' | 'MEDIUM' | 'LOW';
reason: string;
}
export interface UpgradePath {
nodeType: string;
fromVersion: string;
toVersion: string;
direct: boolean;
intermediateVersions: string[];
totalBreakingChanges: number;
autoMigratableChanges: number;
manualRequiredChanges: number;
estimatedEffort: 'LOW' | 'MEDIUM' | 'HIGH';
steps: UpgradeStep[];
}
export interface UpgradeStep {
fromVersion: string;
toVersion: string;
breakingChanges: number;
migrationHints: string[];
}
export declare class NodeVersionService {
private nodeRepository;
private breakingChangeDetector;
private versionCache;
private cacheTTL;
private cacheTimestamps;
constructor(nodeRepository: NodeRepository, breakingChangeDetector: BreakingChangeDetector);
getAvailableVersions(nodeType: string): NodeVersion[];
getLatestVersion(nodeType: string): string | null;
compareVersions(currentVersion: string, latestVersion: string): number;
analyzeVersion(nodeType: string, currentVersion: string): VersionComparison;
private calculateVersionGap;
suggestUpgradePath(nodeType: string, currentVersion: string): Promise<UpgradePath | null>;
versionExists(nodeType: string, version: string): boolean;
getVersionMetadata(nodeType: string, version: string): NodeVersion | null;
clearCache(nodeType?: string): void;
private getCachedVersions;
private cacheVersions;
}
//# sourceMappingURL=node-version-service.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"node-version-service.d.ts","sourceRoot":"","sources":["../../src/services/node-version-service.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAEpE,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,eAAe,EAAE,GAAG,EAAE,CAAC;IACvB,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,UAAU,CAAC,EAAE,IAAI,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IACtC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,eAAe,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC3C,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAKD,qBAAa,kBAAkB;IAM3B,OAAO,CAAC,cAAc;IACtB,OAAO,CAAC,sBAAsB;IANhC,OAAO,CAAC,YAAY,CAAyC;IAC7D,OAAO,CAAC,QAAQ,CAAyB;IACzC,OAAO,CAAC,eAAe,CAAkC;gBAG/C,cAAc,EAAE,cAAc,EAC9B,sBAAsB,EAAE,sBAAsB;IAMxD,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,EAAE;IAiBrD,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAqBjD,eAAe,CAAC,cAAc,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM;IAkBtE,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,iBAAiB;IA6E3E,OAAO,CAAC,mBAAmB;IAmBrB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAuG/F,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO;IAQzD,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI;IAQzE,UAAU,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI;IAanC,OAAO,CAAC,iBAAiB;IAiBzB,OAAO,CAAC,aAAa;CAItB"}

215
dist/services/node-version-service.js vendored Normal file
View File

@@ -0,0 +1,215 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NodeVersionService = void 0;
class NodeVersionService {
constructor(nodeRepository, breakingChangeDetector) {
this.nodeRepository = nodeRepository;
this.breakingChangeDetector = breakingChangeDetector;
this.versionCache = new Map();
this.cacheTTL = 5 * 60 * 1000;
this.cacheTimestamps = new Map();
}
getAvailableVersions(nodeType) {
const cached = this.getCachedVersions(nodeType);
if (cached)
return cached;
const versions = this.nodeRepository.getNodeVersions(nodeType);
this.cacheVersions(nodeType, versions);
return versions;
}
getLatestVersion(nodeType) {
const versions = this.getAvailableVersions(nodeType);
if (versions.length === 0) {
const node = this.nodeRepository.getNode(nodeType);
return node?.version || null;
}
const maxVersion = versions.find(v => v.isCurrentMax);
if (maxVersion)
return maxVersion.version;
const sorted = versions.sort((a, b) => this.compareVersions(b.version, a.version));
return sorted[0]?.version || null;
}
compareVersions(currentVersion, latestVersion) {
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;
}
analyzeVersion(nodeType, currentVersion) {
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.'
};
}
const versionGap = this.calculateVersionGap(currentVersion, latestVersion);
const hasBreakingChanges = this.breakingChangeDetector.hasBreakingChanges(nodeType, currentVersion, latestVersion);
let recommendUpgrade = true;
let confidence = '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
};
}
calculateVersionGap(fromVersion, toVersion) {
const from = fromVersion.split('.').map(Number);
const to = toVersion.split('.').map(Number);
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;
}
async suggestUpgradePath(nodeType, currentVersion) {
const latestVersion = this.getLatestVersion(nodeType);
if (!latestVersion)
return null;
const comparison = this.compareVersions(currentVersion, latestVersion);
if (comparison >= 0)
return null;
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));
const analysis = await this.breakingChangeDetector.analyzeVersionUpgrade(nodeType, currentVersion, latestVersion);
const versionGap = this.calculateVersionGap(currentVersion, latestVersion);
const direct = versionGap <= 1 || !analysis.hasBreakingChanges;
const steps = [];
if (direct || intermediateVersions.length === 0) {
steps.push({
fromVersion: currentVersion,
toVersion: latestVersion,
breakingChanges: analysis.changes.filter(c => c.isBreaking).length,
migrationHints: analysis.recommendations
});
}
else {
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;
}
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
});
}
const totalBreakingChanges = steps.reduce((sum, step) => sum + step.breakingChanges, 0);
let estimatedEffort = '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
};
}
versionExists(nodeType, version) {
const versions = this.getAvailableVersions(nodeType);
return versions.some(v => v.version === version);
}
getVersionMetadata(nodeType, version) {
const versionData = this.nodeRepository.getNodeVersion(nodeType, version);
return versionData;
}
clearCache(nodeType) {
if (nodeType) {
this.versionCache.delete(nodeType);
this.cacheTimestamps.delete(nodeType);
}
else {
this.versionCache.clear();
this.cacheTimestamps.clear();
}
}
getCachedVersions(nodeType) {
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;
}
cacheVersions(nodeType, versions) {
this.versionCache.set(nodeType, versions);
this.cacheTimestamps.set(nodeType, Date.now());
}
}
exports.NodeVersionService = NodeVersionService;
//# sourceMappingURL=node-version-service.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,32 @@
import { NodeRepository } from '../database/node-repository';
export interface OperationSuggestion {
value: string;
confidence: number;
reason: string;
resource?: string;
description?: string;
}
export declare class OperationSimilarityService {
private static readonly CACHE_DURATION_MS;
private static readonly MIN_CONFIDENCE;
private static readonly MAX_SUGGESTIONS;
private static readonly CONFIDENCE_THRESHOLDS;
private repository;
private operationCache;
private suggestionCache;
private commonPatterns;
constructor(repository: NodeRepository);
private cleanupExpiredEntries;
private initializeCommonPatterns;
findSimilarOperations(nodeType: string, invalidOperation: string, resource?: string, maxSuggestions?: number): OperationSuggestion[];
private getOperationValue;
private getResourceValue;
private getNodeOperations;
private getNodePatterns;
private calculateSimilarity;
private levenshteinDistance;
private areCommonVariations;
private getSimilarityReason;
clearCache(): void;
}
//# sourceMappingURL=operation-similarity-service.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"operation-similarity-service.d.ts","sourceRoot":"","sources":["../../src/services/operation-similarity-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAI7D,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AASD,qBAAa,0BAA0B;IACrC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAiB;IAC1D,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAO;IAC7C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAK;IAG5C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAMlC;IAEX,OAAO,CAAC,UAAU,CAAiB;IACnC,OAAO,CAAC,cAAc,CAAoE;IAC1F,OAAO,CAAC,eAAe,CAAiD;IACxE,OAAO,CAAC,cAAc,CAAkC;gBAE5C,UAAU,EAAE,cAAc;IAStC,OAAO,CAAC,qBAAqB;IAwB7B,OAAO,CAAC,wBAAwB;IAmEhC,qBAAqB,CACnB,QAAQ,EAAE,MAAM,EAChB,gBAAgB,EAAE,MAAM,EACxB,QAAQ,CAAC,EAAE,MAAM,EACjB,cAAc,GAAE,MAAmD,GAClE,mBAAmB,EAAE;IA0FxB,OAAO,CAAC,iBAAiB;IAezB,OAAO,CAAC,gBAAgB;IAaxB,OAAO,CAAC,iBAAiB;IA+EzB,OAAO,CAAC,eAAe;IAuBvB,OAAO,CAAC,mBAAmB;IAuC3B,OAAO,CAAC,mBAAmB;IA4B3B,OAAO,CAAC,mBAAmB;IAgD3B,OAAO,CAAC,mBAAmB;IAmB3B,UAAU,IAAI,IAAI;CAInB"}

View File

@@ -0,0 +1,341 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.OperationSimilarityService = void 0;
const logger_1 = require("../utils/logger");
const validation_service_error_1 = require("../errors/validation-service-error");
class OperationSimilarityService {
constructor(repository) {
this.operationCache = new Map();
this.suggestionCache = new Map();
this.repository = repository;
this.commonPatterns = this.initializeCommonPatterns();
}
cleanupExpiredEntries() {
const now = Date.now();
for (const [key, value] of this.operationCache.entries()) {
if (now - value.timestamp >= OperationSimilarityService.CACHE_DURATION_MS) {
this.operationCache.delete(key);
}
}
if (this.suggestionCache.size > 100) {
const entries = Array.from(this.suggestionCache.entries());
this.suggestionCache.clear();
entries.slice(-50).forEach(([key, value]) => {
this.suggestionCache.set(key, value);
});
}
}
initializeCommonPatterns() {
const patterns = new Map();
patterns.set('googleDrive', [
{ pattern: 'listFiles', suggestion: 'search', confidence: 0.85, reason: 'Use "search" with resource: "fileFolder" to list files' },
{ pattern: 'uploadFile', suggestion: 'upload', confidence: 0.95, reason: 'Use "upload" instead of "uploadFile"' },
{ pattern: 'deleteFile', suggestion: 'deleteFile', confidence: 1.0, reason: 'Exact match' },
{ pattern: 'downloadFile', suggestion: 'download', confidence: 0.95, reason: 'Use "download" instead of "downloadFile"' },
{ pattern: 'getFile', suggestion: 'download', confidence: 0.8, reason: 'Use "download" to retrieve file content' },
{ pattern: 'listFolders', suggestion: 'search', confidence: 0.85, reason: 'Use "search" with resource: "fileFolder"' },
]);
patterns.set('slack', [
{ pattern: 'sendMessage', suggestion: 'send', confidence: 0.95, reason: 'Use "send" instead of "sendMessage"' },
{ pattern: 'getMessage', suggestion: 'get', confidence: 0.9, reason: 'Use "get" to retrieve messages' },
{ pattern: 'postMessage', suggestion: 'send', confidence: 0.9, reason: 'Use "send" to post messages' },
{ pattern: 'deleteMessage', suggestion: 'delete', confidence: 0.95, reason: 'Use "delete" instead of "deleteMessage"' },
{ pattern: 'createChannel', suggestion: 'create', confidence: 0.9, reason: 'Use "create" with resource: "channel"' },
]);
patterns.set('database', [
{ pattern: 'selectData', suggestion: 'select', confidence: 0.95, reason: 'Use "select" instead of "selectData"' },
{ pattern: 'insertData', suggestion: 'insert', confidence: 0.95, reason: 'Use "insert" instead of "insertData"' },
{ pattern: 'updateData', suggestion: 'update', confidence: 0.95, reason: 'Use "update" instead of "updateData"' },
{ pattern: 'deleteData', suggestion: 'delete', confidence: 0.95, reason: 'Use "delete" instead of "deleteData"' },
{ pattern: 'query', suggestion: 'select', confidence: 0.7, reason: 'Use "select" for queries' },
{ pattern: 'fetch', suggestion: 'select', confidence: 0.7, reason: 'Use "select" to fetch data' },
]);
patterns.set('httpRequest', [
{ pattern: 'fetch', suggestion: 'GET', confidence: 0.8, reason: 'Use "GET" method for fetching data' },
{ pattern: 'send', suggestion: 'POST', confidence: 0.7, reason: 'Use "POST" method for sending data' },
{ pattern: 'create', suggestion: 'POST', confidence: 0.8, reason: 'Use "POST" method for creating resources' },
{ pattern: 'update', suggestion: 'PUT', confidence: 0.8, reason: 'Use "PUT" method for updating resources' },
{ pattern: 'delete', suggestion: 'DELETE', confidence: 0.9, reason: 'Use "DELETE" method' },
]);
patterns.set('generic', [
{ pattern: 'list', suggestion: 'get', confidence: 0.6, reason: 'Consider using "get" or "search"' },
{ pattern: 'retrieve', suggestion: 'get', confidence: 0.8, reason: 'Use "get" to retrieve data' },
{ pattern: 'fetch', suggestion: 'get', confidence: 0.8, reason: 'Use "get" to fetch data' },
{ pattern: 'remove', suggestion: 'delete', confidence: 0.85, reason: 'Use "delete" to remove items' },
{ pattern: 'add', suggestion: 'create', confidence: 0.7, reason: 'Use "create" to add new items' },
]);
return patterns;
}
findSimilarOperations(nodeType, invalidOperation, resource, maxSuggestions = OperationSimilarityService.MAX_SUGGESTIONS) {
if (Math.random() < 0.1) {
this.cleanupExpiredEntries();
}
const cacheKey = `${nodeType}:${invalidOperation}:${resource || ''}`;
if (this.suggestionCache.has(cacheKey)) {
return this.suggestionCache.get(cacheKey);
}
const suggestions = [];
let nodeInfo;
try {
nodeInfo = this.repository.getNode(nodeType);
if (!nodeInfo) {
return [];
}
}
catch (error) {
logger_1.logger.warn(`Error getting node ${nodeType}:`, error);
return [];
}
const validOperations = this.getNodeOperations(nodeType, resource);
for (const op of validOperations) {
const opValue = this.getOperationValue(op);
if (opValue.toLowerCase() === invalidOperation.toLowerCase()) {
return [];
}
}
const nodePatterns = this.getNodePatterns(nodeType);
for (const pattern of nodePatterns) {
if (pattern.pattern.toLowerCase() === invalidOperation.toLowerCase()) {
const exists = validOperations.some(op => {
const opValue = this.getOperationValue(op);
return opValue === pattern.suggestion;
});
if (exists) {
suggestions.push({
value: pattern.suggestion,
confidence: pattern.confidence,
reason: pattern.reason,
resource
});
}
}
}
for (const op of validOperations) {
const opValue = this.getOperationValue(op);
const similarity = this.calculateSimilarity(invalidOperation, opValue);
if (similarity >= OperationSimilarityService.MIN_CONFIDENCE) {
if (!suggestions.some(s => s.value === opValue)) {
suggestions.push({
value: opValue,
confidence: similarity,
reason: this.getSimilarityReason(similarity, invalidOperation, opValue),
resource: typeof op === 'object' ? op.resource : undefined,
description: typeof op === 'object' ? (op.description || op.name) : undefined
});
}
}
}
suggestions.sort((a, b) => b.confidence - a.confidence);
const topSuggestions = suggestions.slice(0, maxSuggestions);
this.suggestionCache.set(cacheKey, topSuggestions);
return topSuggestions;
}
getOperationValue(op) {
if (typeof op === 'string') {
return op;
}
if (typeof op === 'object' && op !== null) {
return op.operation || op.value || '';
}
return '';
}
getResourceValue(resource) {
if (typeof resource === 'string') {
return resource;
}
if (typeof resource === 'object' && resource !== null) {
return resource.value || '';
}
return '';
}
getNodeOperations(nodeType, resource) {
if (Math.random() < 0.05) {
this.cleanupExpiredEntries();
}
const cacheKey = `${nodeType}:${resource || 'all'}`;
const cached = this.operationCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < OperationSimilarityService.CACHE_DURATION_MS) {
return cached.operations;
}
const nodeInfo = this.repository.getNode(nodeType);
if (!nodeInfo)
return [];
let operations = [];
try {
const opsData = nodeInfo.operations;
if (typeof opsData === 'string') {
try {
operations = JSON.parse(opsData);
}
catch (parseError) {
logger_1.logger.error(`JSON parse error for operations in ${nodeType}:`, parseError);
throw validation_service_error_1.ValidationServiceError.jsonParseError(nodeType, parseError);
}
}
else if (Array.isArray(opsData)) {
operations = opsData;
}
else if (opsData && typeof opsData === 'object') {
operations = Object.values(opsData).flat();
}
}
catch (error) {
if (error instanceof validation_service_error_1.ValidationServiceError) {
throw error;
}
logger_1.logger.warn(`Failed to process operations for ${nodeType}:`, error);
}
try {
const properties = nodeInfo.properties || [];
for (const prop of properties) {
if (prop.name === 'operation' && prop.options) {
if (prop.displayOptions?.show?.resource) {
const allowedResources = Array.isArray(prop.displayOptions.show.resource)
? prop.displayOptions.show.resource
: [prop.displayOptions.show.resource];
if (resource && !allowedResources.includes(resource)) {
continue;
}
}
operations.push(...prop.options.map((opt) => ({
operation: opt.value,
name: opt.name,
description: opt.description,
resource
})));
}
}
}
catch (error) {
logger_1.logger.warn(`Failed to extract operations from properties for ${nodeType}:`, error);
}
this.operationCache.set(cacheKey, { operations, timestamp: Date.now() });
return operations;
}
getNodePatterns(nodeType) {
const patterns = [];
if (nodeType.includes('googleDrive')) {
patterns.push(...(this.commonPatterns.get('googleDrive') || []));
}
else if (nodeType.includes('slack')) {
patterns.push(...(this.commonPatterns.get('slack') || []));
}
else if (nodeType.includes('postgres') || nodeType.includes('mysql') || nodeType.includes('mongodb')) {
patterns.push(...(this.commonPatterns.get('database') || []));
}
else if (nodeType.includes('httpRequest')) {
patterns.push(...(this.commonPatterns.get('httpRequest') || []));
}
patterns.push(...(this.commonPatterns.get('generic') || []));
return patterns;
}
calculateSimilarity(str1, str2) {
const s1 = str1.toLowerCase();
const s2 = str2.toLowerCase();
if (s1 === s2)
return 1.0;
if (s1.includes(s2) || s2.includes(s1)) {
const ratio = Math.min(s1.length, s2.length) / Math.max(s1.length, s2.length);
return Math.max(OperationSimilarityService.CONFIDENCE_THRESHOLDS.MIN_SUBSTRING, ratio);
}
const distance = this.levenshteinDistance(s1, s2);
const maxLength = Math.max(s1.length, s2.length);
let similarity = 1 - (distance / maxLength);
if (distance === 1 && maxLength <= 5) {
similarity = Math.max(similarity, 0.75);
}
else if (distance === 2 && maxLength <= 5) {
similarity = Math.max(similarity, 0.72);
}
if (this.areCommonVariations(s1, s2)) {
return Math.min(1.0, similarity + 0.2);
}
return similarity;
}
levenshteinDistance(str1, str2) {
const m = str1.length;
const n = str2.length;
const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
for (let i = 0; i <= m; i++)
dp[i][0] = i;
for (let j = 0; j <= n; j++)
dp[0][j] = j;
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (str1[i - 1] === str2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
}
else {
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 1);
}
}
}
return dp[m][n];
}
areCommonVariations(str1, str2) {
if (str1 === '' || str2 === '' || str1 === str2) {
return false;
}
const commonPrefixes = ['get', 'set', 'create', 'delete', 'update', 'send', 'fetch'];
const commonSuffixes = ['data', 'item', 'record', 'message', 'file', 'folder'];
for (const prefix of commonPrefixes) {
if ((str1.startsWith(prefix) && !str2.startsWith(prefix)) ||
(!str1.startsWith(prefix) && str2.startsWith(prefix))) {
const s1Clean = str1.startsWith(prefix) ? str1.slice(prefix.length) : str1;
const s2Clean = str2.startsWith(prefix) ? str2.slice(prefix.length) : str2;
if ((str1.startsWith(prefix) && s1Clean !== str1) || (str2.startsWith(prefix) && s2Clean !== str2)) {
if (s1Clean === s2Clean || this.levenshteinDistance(s1Clean, s2Clean) <= 2) {
return true;
}
}
}
}
for (const suffix of commonSuffixes) {
if ((str1.endsWith(suffix) && !str2.endsWith(suffix)) ||
(!str1.endsWith(suffix) && str2.endsWith(suffix))) {
const s1Clean = str1.endsWith(suffix) ? str1.slice(0, -suffix.length) : str1;
const s2Clean = str2.endsWith(suffix) ? str2.slice(0, -suffix.length) : str2;
if ((str1.endsWith(suffix) && s1Clean !== str1) || (str2.endsWith(suffix) && s2Clean !== str2)) {
if (s1Clean === s2Clean || this.levenshteinDistance(s1Clean, s2Clean) <= 2) {
return true;
}
}
}
}
return false;
}
getSimilarityReason(confidence, invalid, valid) {
const { VERY_HIGH, HIGH, MEDIUM } = OperationSimilarityService.CONFIDENCE_THRESHOLDS;
if (confidence >= VERY_HIGH) {
return 'Almost exact match - likely a typo';
}
else if (confidence >= HIGH) {
return 'Very similar - common variation';
}
else if (confidence >= MEDIUM) {
return 'Similar operation';
}
else if (invalid.includes(valid) || valid.includes(invalid)) {
return 'Partial match';
}
else {
return 'Possibly related operation';
}
}
clearCache() {
this.operationCache.clear();
this.suggestionCache.clear();
}
}
exports.OperationSimilarityService = OperationSimilarityService;
OperationSimilarityService.CACHE_DURATION_MS = 5 * 60 * 1000;
OperationSimilarityService.MIN_CONFIDENCE = 0.3;
OperationSimilarityService.MAX_SUGGESTIONS = 5;
OperationSimilarityService.CONFIDENCE_THRESHOLDS = {
EXACT: 1.0,
VERY_HIGH: 0.95,
HIGH: 0.8,
MEDIUM: 0.6,
MIN_SUBSTRING: 0.7
};
//# sourceMappingURL=operation-similarity-service.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,59 @@
import { BreakingChangeDetector } from './breaking-change-detector';
import { MigrationResult } from './node-migration-service';
import { NodeVersionService } from './node-version-service';
export interface PostUpdateGuidance {
nodeId: string;
nodeName: string;
nodeType: string;
oldVersion: string;
newVersion: string;
migrationStatus: 'complete' | 'partial' | 'manual_required';
requiredActions: RequiredAction[];
deprecatedProperties: DeprecatedProperty[];
behaviorChanges: BehaviorChange[];
migrationSteps: string[];
confidence: 'HIGH' | 'MEDIUM' | 'LOW';
estimatedTime: string;
}
export interface RequiredAction {
type: 'ADD_PROPERTY' | 'UPDATE_PROPERTY' | 'CONFIGURE_OPTION' | 'REVIEW_CONFIGURATION';
property: string;
reason: string;
suggestedValue?: any;
currentValue?: any;
documentation?: string;
priority: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW';
}
export interface DeprecatedProperty {
property: string;
status: 'removed' | 'deprecated';
replacement?: string;
action: 'remove' | 'replace' | 'ignore';
impact: 'breaking' | 'warning';
}
export interface BehaviorChange {
aspect: string;
oldBehavior: string;
newBehavior: string;
impact: 'HIGH' | 'MEDIUM' | 'LOW';
actionRequired: boolean;
recommendation: string;
}
export declare class PostUpdateValidator {
private versionService;
private breakingChangeDetector;
constructor(versionService: NodeVersionService, breakingChangeDetector: BreakingChangeDetector);
generateGuidance(nodeId: string, nodeName: string, nodeType: string, oldVersion: string, newVersion: string, migrationResult: MigrationResult): Promise<PostUpdateGuidance>;
private determineMigrationStatus;
private generateRequiredActions;
private identifyDeprecatedProperties;
private documentBehaviorChanges;
private generateMigrationSteps;
private mapChangeTypeToActionType;
private mapSeverityToPriority;
private getPropertyDocumentation;
private calculateConfidence;
private estimateTime;
generateSummary(guidance: PostUpdateGuidance): string;
}
//# sourceMappingURL=post-update-validator.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"post-update-validator.d.ts","sourceRoot":"","sources":["../../src/services/post-update-validator.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,sBAAsB,EAAkB,MAAM,4BAA4B,CAAC;AACpF,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAE5D,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,UAAU,GAAG,SAAS,GAAG,iBAAiB,CAAC;IAC5D,eAAe,EAAE,cAAc,EAAE,CAAC;IAClC,oBAAoB,EAAE,kBAAkB,EAAE,CAAC;IAC3C,eAAe,EAAE,cAAc,EAAE,CAAC;IAClC,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IACtC,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,cAAc,GAAG,iBAAiB,GAAG,kBAAkB,GAAG,sBAAsB,CAAC;IACvF,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,GAAG,CAAC;IACrB,YAAY,CAAC,EAAE,GAAG,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;CAClD;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,SAAS,GAAG,YAAY,CAAC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;IACxC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAC;CAChC;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IAClC,cAAc,EAAE,OAAO,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,qBAAa,mBAAmB;IAE5B,OAAO,CAAC,cAAc;IACtB,OAAO,CAAC,sBAAsB;gBADtB,cAAc,EAAE,kBAAkB,EAClC,sBAAsB,EAAE,sBAAsB;IAMlD,gBAAgB,CACpB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,eAAe,EAAE,eAAe,GAC/B,OAAO,CAAC,kBAAkB,CAAC;IAsD9B,OAAO,CAAC,wBAAwB;IAoBhC,OAAO,CAAC,uBAAuB;IA4B/B,OAAO,CAAC,4BAA4B;IAqBpC,OAAO,CAAC,uBAAuB;IAuD/B,OAAO,CAAC,sBAAsB;IAmE9B,OAAO,CAAC,yBAAyB;IAmBjC,OAAO,CAAC,qBAAqB;IAU7B,OAAO,CAAC,wBAAwB;IAQhC,OAAO,CAAC,mBAAmB;IAkB3B,OAAO,CAAC,YAAY;IAoBpB,eAAe,CAAC,QAAQ,EAAE,kBAAkB,GAAG,MAAM;CA2BtD"}

231
dist/services/post-update-validator.js vendored Normal file
View File

@@ -0,0 +1,231 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PostUpdateValidator = void 0;
class PostUpdateValidator {
constructor(versionService, breakingChangeDetector) {
this.versionService = versionService;
this.breakingChangeDetector = breakingChangeDetector;
}
async generateGuidance(nodeId, nodeName, nodeType, oldVersion, newVersion, migrationResult) {
const analysis = await this.breakingChangeDetector.analyzeVersionUpgrade(nodeType, oldVersion, newVersion);
const migrationStatus = this.determineMigrationStatus(migrationResult, analysis.changes);
const requiredActions = this.generateRequiredActions(migrationResult, analysis.changes, nodeType);
const deprecatedProperties = this.identifyDeprecatedProperties(analysis.changes);
const behaviorChanges = this.documentBehaviorChanges(nodeType, oldVersion, newVersion);
const migrationSteps = this.generateMigrationSteps(requiredActions, deprecatedProperties, behaviorChanges);
const confidence = this.calculateConfidence(requiredActions, migrationStatus);
const estimatedTime = this.estimateTime(requiredActions, behaviorChanges);
return {
nodeId,
nodeName,
nodeType,
oldVersion,
newVersion,
migrationStatus,
requiredActions,
deprecatedProperties,
behaviorChanges,
migrationSteps,
confidence,
estimatedTime
};
}
determineMigrationStatus(migrationResult, changes) {
if (migrationResult.remainingIssues.length === 0) {
return 'complete';
}
const criticalIssues = changes.filter(c => c.isBreaking && !c.autoMigratable);
if (criticalIssues.length > 0) {
return 'manual_required';
}
return 'partial';
}
generateRequiredActions(migrationResult, changes, nodeType) {
const actions = [];
const manualChanges = changes.filter(c => !c.autoMigratable);
for (const change of manualChanges) {
actions.push({
type: this.mapChangeTypeToActionType(change.changeType),
property: change.propertyName,
reason: change.migrationHint,
suggestedValue: change.newValue,
currentValue: change.oldValue,
documentation: this.getPropertyDocumentation(nodeType, change.propertyName),
priority: this.mapSeverityToPriority(change.severity)
});
}
return actions;
}
identifyDeprecatedProperties(changes) {
const deprecated = [];
for (const change of changes) {
if (change.changeType === 'removed') {
deprecated.push({
property: change.propertyName,
status: 'removed',
replacement: change.migrationStrategy?.targetProperty,
action: change.autoMigratable ? 'remove' : 'replace',
impact: change.isBreaking ? 'breaking' : 'warning'
});
}
}
return deprecated;
}
documentBehaviorChanges(nodeType, oldVersion, newVersion) {
const changes = [];
if (nodeType === 'n8n-nodes-base.executeWorkflow') {
if (this.versionService.compareVersions(oldVersion, '1.1') < 0 &&
this.versionService.compareVersions(newVersion, '1.1') >= 0) {
changes.push({
aspect: 'Data passing to sub-workflows',
oldBehavior: 'Automatic data passing - all data from parent workflow automatically available',
newBehavior: 'Explicit field mapping required - must define inputFieldMapping to pass specific fields',
impact: 'HIGH',
actionRequired: true,
recommendation: 'Define inputFieldMapping with specific field mappings between parent and child workflows. Review data dependencies.'
});
}
}
if (nodeType === 'n8n-nodes-base.webhook') {
if (this.versionService.compareVersions(oldVersion, '2.1') < 0 &&
this.versionService.compareVersions(newVersion, '2.1') >= 0) {
changes.push({
aspect: 'Webhook persistence',
oldBehavior: 'Webhook URL changes on workflow updates',
newBehavior: 'Stable webhook URL via webhookId field',
impact: 'MEDIUM',
actionRequired: false,
recommendation: 'Webhook URLs now remain stable across workflow updates. Update external systems if needed.'
});
}
if (this.versionService.compareVersions(oldVersion, '2.0') < 0 &&
this.versionService.compareVersions(newVersion, '2.0') >= 0) {
changes.push({
aspect: 'Response handling',
oldBehavior: 'Automatic response after webhook trigger',
newBehavior: 'Configurable response mode (onReceived vs lastNode)',
impact: 'MEDIUM',
actionRequired: true,
recommendation: 'Review responseMode setting. Use "onReceived" for immediate responses or "lastNode" to wait for workflow completion.'
});
}
}
return changes;
}
generateMigrationSteps(requiredActions, deprecatedProperties, behaviorChanges) {
const steps = [];
let stepNumber = 1;
if (deprecatedProperties.length > 0) {
steps.push(`Step ${stepNumber++}: Remove deprecated properties`);
for (const dep of deprecatedProperties) {
steps.push(` - Remove "${dep.property}" ${dep.replacement ? `(use "${dep.replacement}" instead)` : ''}`);
}
}
const criticalActions = requiredActions.filter(a => a.priority === 'CRITICAL');
if (criticalActions.length > 0) {
steps.push(`Step ${stepNumber++}: Address critical configuration requirements`);
for (const action of criticalActions) {
steps.push(` - ${action.property}: ${action.reason}`);
if (action.suggestedValue !== undefined) {
steps.push(` Suggested value: ${JSON.stringify(action.suggestedValue)}`);
}
}
}
const highActions = requiredActions.filter(a => a.priority === 'HIGH');
if (highActions.length > 0) {
steps.push(`Step ${stepNumber++}: Configure required properties`);
for (const action of highActions) {
steps.push(` - ${action.property}: ${action.reason}`);
}
}
const actionRequiredChanges = behaviorChanges.filter(c => c.actionRequired);
if (actionRequiredChanges.length > 0) {
steps.push(`Step ${stepNumber++}: Adapt to behavior changes`);
for (const change of actionRequiredChanges) {
steps.push(` - ${change.aspect}: ${change.recommendation}`);
}
}
const otherActions = requiredActions.filter(a => a.priority === 'MEDIUM' || a.priority === 'LOW');
if (otherActions.length > 0) {
steps.push(`Step ${stepNumber++}: Review optional configurations`);
for (const action of otherActions) {
steps.push(` - ${action.property}: ${action.reason}`);
}
}
steps.push(`Step ${stepNumber}: Test workflow execution`);
steps.push(' - Validate all node configurations');
steps.push(' - Run a test execution');
steps.push(' - Verify expected behavior');
return steps;
}
mapChangeTypeToActionType(changeType) {
switch (changeType) {
case 'added':
return 'ADD_PROPERTY';
case 'requirement_changed':
case 'type_changed':
return 'UPDATE_PROPERTY';
case 'default_changed':
return 'CONFIGURE_OPTION';
default:
return 'REVIEW_CONFIGURATION';
}
}
mapSeverityToPriority(severity) {
if (severity === 'HIGH')
return 'CRITICAL';
return severity;
}
getPropertyDocumentation(nodeType, propertyName) {
return `See n8n documentation for ${nodeType} - ${propertyName}`;
}
calculateConfidence(requiredActions, migrationStatus) {
if (migrationStatus === 'complete')
return 'HIGH';
const criticalActions = requiredActions.filter(a => a.priority === 'CRITICAL');
if (migrationStatus === 'manual_required' || criticalActions.length > 3) {
return 'LOW';
}
return 'MEDIUM';
}
estimateTime(requiredActions, behaviorChanges) {
const criticalCount = requiredActions.filter(a => a.priority === 'CRITICAL').length;
const highCount = requiredActions.filter(a => a.priority === 'HIGH').length;
const behaviorCount = behaviorChanges.filter(c => c.actionRequired).length;
const totalComplexity = criticalCount * 5 + highCount * 3 + behaviorCount * 2;
if (totalComplexity === 0)
return '< 1 minute';
if (totalComplexity <= 5)
return '2-5 minutes';
if (totalComplexity <= 10)
return '5-10 minutes';
if (totalComplexity <= 20)
return '10-20 minutes';
return '20+ minutes';
}
generateSummary(guidance) {
const lines = [];
lines.push(`Node "${guidance.nodeName}" upgraded from v${guidance.oldVersion} to v${guidance.newVersion}`);
lines.push(`Status: ${guidance.migrationStatus.toUpperCase()}`);
lines.push(`Confidence: ${guidance.confidence}`);
lines.push(`Estimated time: ${guidance.estimatedTime}`);
if (guidance.requiredActions.length > 0) {
lines.push(`\nRequired actions: ${guidance.requiredActions.length}`);
for (const action of guidance.requiredActions.slice(0, 3)) {
lines.push(` - [${action.priority}] ${action.property}: ${action.reason}`);
}
if (guidance.requiredActions.length > 3) {
lines.push(` ... and ${guidance.requiredActions.length - 3} more`);
}
}
if (guidance.behaviorChanges.length > 0) {
lines.push(`\nBehavior changes: ${guidance.behaviorChanges.length}`);
for (const change of guidance.behaviorChanges) {
lines.push(` - ${change.aspect}: ${change.newBehavior}`);
}
}
return lines.join('\n');
}
}
exports.PostUpdateValidator = PostUpdateValidator;
//# sourceMappingURL=post-update-validator.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,36 @@
export interface PropertyDependency {
property: string;
displayName: string;
dependsOn: DependencyCondition[];
showWhen?: Record<string, any>;
hideWhen?: Record<string, any>;
enablesProperties?: string[];
disablesProperties?: string[];
notes?: string[];
}
export interface DependencyCondition {
property: string;
values: any[];
condition: 'equals' | 'not_equals' | 'includes' | 'not_includes';
description?: string;
}
export interface DependencyAnalysis {
totalProperties: number;
propertiesWithDependencies: number;
dependencies: PropertyDependency[];
dependencyGraph: Record<string, string[]>;
suggestions: string[];
}
export declare class PropertyDependencies {
static analyze(properties: any[]): DependencyAnalysis;
private static extractDependency;
private static generateConditionDescription;
private static generateSuggestions;
static getVisibilityImpact(properties: any[], config: Record<string, any>): {
visible: string[];
hidden: string[];
reasons: Record<string, string>;
};
private static checkVisibility;
}
//# sourceMappingURL=property-dependencies.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"property-dependencies.d.ts","sourceRoot":"","sources":["../../src/services/property-dependencies.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,mBAAmB,EAAE,CAAC;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC/B,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,GAAG,EAAE,CAAC;IACd,SAAS,EAAE,QAAQ,GAAG,YAAY,GAAG,UAAU,GAAG,cAAc,CAAC;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,eAAe,EAAE,MAAM,CAAC;IACxB,0BAA0B,EAAE,MAAM,CAAC;IACnC,YAAY,EAAE,kBAAkB,EAAE,CAAC;IACnC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC1C,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,qBAAa,oBAAoB;IAI/B,MAAM,CAAC,OAAO,CAAC,UAAU,EAAE,GAAG,EAAE,GAAG,kBAAkB;IAyCrD,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAmDhC,OAAO,CAAC,MAAM,CAAC,4BAA4B;IA2B3C,OAAO,CAAC,MAAM,CAAC,mBAAmB;IA4ClC,MAAM,CAAC,mBAAmB,CACxB,UAAU,EAAE,GAAG,EAAE,EACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC1B;QAAE,OAAO,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE;IAyB3E,OAAO,CAAC,MAAM,CAAC,eAAe;CAwC/B"}

168
dist/services/property-dependencies.js vendored Normal file
View File

@@ -0,0 +1,168 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PropertyDependencies = void 0;
class PropertyDependencies {
static analyze(properties) {
const dependencies = [];
const dependencyGraph = {};
const suggestions = [];
for (const prop of properties) {
if (prop.displayOptions?.show || prop.displayOptions?.hide) {
const dependency = this.extractDependency(prop, properties);
dependencies.push(dependency);
for (const condition of dependency.dependsOn) {
if (!dependencyGraph[condition.property]) {
dependencyGraph[condition.property] = [];
}
dependencyGraph[condition.property].push(prop.name);
}
}
}
for (const dep of dependencies) {
dep.enablesProperties = dependencyGraph[dep.property] || [];
}
this.generateSuggestions(dependencies, suggestions);
return {
totalProperties: properties.length,
propertiesWithDependencies: dependencies.length,
dependencies,
dependencyGraph,
suggestions
};
}
static extractDependency(prop, allProperties) {
const dependency = {
property: prop.name,
displayName: prop.displayName || prop.name,
dependsOn: [],
showWhen: prop.displayOptions?.show,
hideWhen: prop.displayOptions?.hide,
notes: []
};
if (prop.displayOptions?.show) {
for (const [key, values] of Object.entries(prop.displayOptions.show)) {
const valuesArray = Array.isArray(values) ? values : [values];
dependency.dependsOn.push({
property: key,
values: valuesArray,
condition: 'equals',
description: this.generateConditionDescription(key, valuesArray, 'show', allProperties)
});
}
}
if (prop.displayOptions?.hide) {
for (const [key, values] of Object.entries(prop.displayOptions.hide)) {
const valuesArray = Array.isArray(values) ? values : [values];
dependency.dependsOn.push({
property: key,
values: valuesArray,
condition: 'not_equals',
description: this.generateConditionDescription(key, valuesArray, 'hide', allProperties)
});
}
}
if (prop.type === 'collection' || prop.type === 'fixedCollection') {
dependency.notes?.push('This property contains nested properties that may have their own dependencies');
}
if (dependency.dependsOn.length > 1) {
dependency.notes?.push('Multiple conditions must be met for this property to be visible');
}
return dependency;
}
static generateConditionDescription(property, values, type, allProperties) {
const prop = allProperties.find(p => p.name === property);
const propName = prop?.displayName || property;
if (type === 'show') {
if (values.length === 1) {
return `Visible when ${propName} is set to "${values[0]}"`;
}
else {
return `Visible when ${propName} is one of: ${values.map(v => `"${v}"`).join(', ')}`;
}
}
else {
if (values.length === 1) {
return `Hidden when ${propName} is set to "${values[0]}"`;
}
else {
return `Hidden when ${propName} is one of: ${values.map(v => `"${v}"`).join(', ')}`;
}
}
}
static generateSuggestions(dependencies, suggestions) {
const controllers = new Map();
for (const dep of dependencies) {
for (const condition of dep.dependsOn) {
controllers.set(condition.property, (controllers.get(condition.property) || 0) + 1);
}
}
const sortedControllers = Array.from(controllers.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 3);
if (sortedControllers.length > 0) {
suggestions.push(`Key properties to configure first: ${sortedControllers.map(([prop]) => prop).join(', ')}`);
}
const complexDeps = dependencies.filter(d => d.dependsOn.length > 1);
if (complexDeps.length > 0) {
suggestions.push(`${complexDeps.length} properties have multiple dependencies - check their conditions carefully`);
}
for (const dep of dependencies) {
for (const condition of dep.dependsOn) {
const targetDep = dependencies.find(d => d.property === condition.property);
if (targetDep?.dependsOn.some(c => c.property === dep.property)) {
suggestions.push(`Circular dependency detected between ${dep.property} and ${condition.property}`);
}
}
}
}
static getVisibilityImpact(properties, config) {
const visible = [];
const hidden = [];
const reasons = {};
for (const prop of properties) {
const { isVisible, reason } = this.checkVisibility(prop, config);
if (isVisible) {
visible.push(prop.name);
}
else {
hidden.push(prop.name);
}
if (reason) {
reasons[prop.name] = reason;
}
}
return { visible, hidden, reasons };
}
static checkVisibility(prop, config) {
if (!prop.displayOptions) {
return { isVisible: true };
}
if (prop.displayOptions.show) {
for (const [key, values] of Object.entries(prop.displayOptions.show)) {
const configValue = config[key];
const expectedValues = Array.isArray(values) ? values : [values];
if (!expectedValues.includes(configValue)) {
return {
isVisible: false,
reason: `Hidden because ${key} is "${configValue}" (needs to be ${expectedValues.join(' or ')})`
};
}
}
}
if (prop.displayOptions.hide) {
for (const [key, values] of Object.entries(prop.displayOptions.hide)) {
const configValue = config[key];
const expectedValues = Array.isArray(values) ? values : [values];
if (expectedValues.includes(configValue)) {
return {
isVisible: false,
reason: `Hidden because ${key} is "${configValue}"`
};
}
}
}
return { isVisible: true };
}
}
exports.PropertyDependencies = PropertyDependencies;
//# sourceMappingURL=property-dependencies.js.map

File diff suppressed because one or more lines are too long

44
dist/services/property-filter.d.ts vendored Normal file
View File

@@ -0,0 +1,44 @@
export interface SimplifiedProperty {
name: string;
displayName: string;
type: string;
description: string;
default?: any;
options?: Array<{
value: string;
label: string;
}>;
required?: boolean;
placeholder?: string;
showWhen?: Record<string, any>;
usageHint?: string;
expectedFormat?: {
structure: Record<string, string>;
modes?: string[];
example: Record<string, any>;
};
}
export interface EssentialConfig {
required: string[];
common: string[];
categoryPriority?: string[];
}
export interface FilteredProperties {
required: SimplifiedProperty[];
common: SimplifiedProperty[];
}
export declare class PropertyFilter {
private static ESSENTIAL_PROPERTIES;
static deduplicateProperties(properties: any[]): any[];
static getEssentials(allProperties: any[], nodeType: string): FilteredProperties;
private static extractProperties;
private static findPropertyByName;
private static simplifyProperty;
private static generateUsageHint;
private static extractDescription;
private static generateDescription;
private static inferEssentials;
static searchProperties(allProperties: any[], query: string, maxResults?: number): SimplifiedProperty[];
private static searchPropertiesRecursive;
}
//# sourceMappingURL=property-filter.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"property-filter.d.ts","sourceRoot":"","sources":["../../src/services/property-filter.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,GAAG,CAAC;IACd,OAAO,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE;QACf,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAClC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;KAC9B,CAAC;CACH;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,kBAAkB,EAAE,CAAC;IAC/B,MAAM,EAAE,kBAAkB,EAAE,CAAC;CAC9B;AAED,qBAAa,cAAc;IAKzB,OAAO,CAAC,MAAM,CAAC,oBAAoB,CA4IjC;IAKF,MAAM,CAAC,qBAAqB,CAAC,UAAU,EAAE,GAAG,EAAE,GAAG,GAAG,EAAE;IAyBtD,MAAM,CAAC,aAAa,CAAC,aAAa,EAAE,GAAG,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,kBAAkB;IA6BhF,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAwBhC,OAAO,CAAC,MAAM,CAAC,kBAAkB;IA6BjC,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAiE/B,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAgChC,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAmBjC,OAAO,CAAC,MAAM,CAAC,mBAAmB;IA+DlC,OAAO,CAAC,MAAM,CAAC,eAAe;IAuD9B,MAAM,CAAC,gBAAgB,CACrB,aAAa,EAAE,GAAG,EAAE,EACpB,KAAK,EAAE,MAAM,EACb,UAAU,GAAE,MAAW,GACtB,kBAAkB,EAAE;IAwBvB,OAAO,CAAC,MAAM,CAAC,yBAAyB;CAkDzC"}

395
dist/services/property-filter.js vendored Normal file
View File

@@ -0,0 +1,395 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PropertyFilter = void 0;
class PropertyFilter {
static deduplicateProperties(properties) {
const seen = new Map();
return properties.filter(prop => {
if (!prop || !prop.name) {
return false;
}
const conditions = JSON.stringify(prop.displayOptions || {});
const key = `${prop.name}_${conditions}`;
if (seen.has(key)) {
return false;
}
seen.set(key, prop);
return true;
});
}
static getEssentials(allProperties, nodeType) {
if (!allProperties) {
return { required: [], common: [] };
}
const uniqueProperties = this.deduplicateProperties(allProperties);
const config = this.ESSENTIAL_PROPERTIES[nodeType];
if (!config) {
return this.inferEssentials(uniqueProperties);
}
const required = this.extractProperties(uniqueProperties, config.required, true);
const requiredNames = new Set(required.map(p => p.name));
const common = this.extractProperties(uniqueProperties, config.common, false)
.filter(p => !requiredNames.has(p.name));
return { required, common };
}
static extractProperties(allProperties, propertyNames, markAsRequired) {
const extracted = [];
for (const name of propertyNames) {
const property = this.findPropertyByName(allProperties, name);
if (property) {
const simplified = this.simplifyProperty(property);
if (markAsRequired) {
simplified.required = true;
}
extracted.push(simplified);
}
}
return extracted;
}
static findPropertyByName(properties, name) {
for (const prop of properties) {
if (prop.name === name) {
return prop;
}
if (prop.type === 'collection' && prop.options) {
const found = this.findPropertyByName(prop.options, name);
if (found)
return found;
}
if (prop.type === 'fixedCollection' && prop.options) {
for (const option of prop.options) {
if (option.values) {
const found = this.findPropertyByName(option.values, name);
if (found)
return found;
}
}
}
}
return undefined;
}
static simplifyProperty(prop) {
const simplified = {
name: prop.name,
displayName: prop.displayName || prop.name,
type: prop.type || 'string',
description: this.extractDescription(prop),
required: prop.required || false
};
if (prop.default !== undefined &&
typeof prop.default !== 'object' ||
prop.type === 'options' ||
prop.type === 'multiOptions') {
simplified.default = prop.default;
}
if (prop.placeholder) {
simplified.placeholder = prop.placeholder;
}
if (prop.options && Array.isArray(prop.options)) {
const limitedOptions = prop.options.slice(0, 20);
simplified.options = limitedOptions.map((opt) => {
if (typeof opt === 'string') {
return { value: opt, label: opt };
}
return {
value: opt.value || opt.name,
label: opt.name || opt.value || opt.displayName
};
});
}
if (prop.type === 'resourceLocator') {
const modes = prop.modes?.map((m) => m.name || m) || ['list', 'id'];
const defaultValue = prop.default?.value || 'your-resource-id';
simplified.expectedFormat = {
structure: { mode: 'string', value: 'string' },
modes,
example: { mode: 'id', value: defaultValue }
};
}
if (prop.displayOptions?.show) {
const conditions = Object.keys(prop.displayOptions.show);
if (conditions.length <= 2) {
simplified.showWhen = prop.displayOptions.show;
}
}
simplified.usageHint = this.generateUsageHint(prop);
return simplified;
}
static generateUsageHint(prop) {
if (prop.name.toLowerCase().includes('url') || prop.name === 'endpoint') {
return 'Enter the full URL including https://';
}
if (prop.name.includes('auth') || prop.name.includes('credential')) {
return 'Select authentication method or credentials';
}
if (prop.type === 'json' || prop.name.includes('json')) {
return 'Enter valid JSON data';
}
if (prop.type === 'code' || prop.name.includes('code')) {
return 'Enter your code here';
}
if (prop.type === 'boolean' && prop.displayOptions) {
return 'Enabling this will show additional options';
}
return undefined;
}
static extractDescription(prop) {
const description = prop.description ||
prop.hint ||
prop.placeholder ||
prop.displayName ||
'';
if (!description) {
return this.generateDescription(prop);
}
return description;
}
static generateDescription(prop) {
const name = prop.name.toLowerCase();
const type = prop.type;
const commonDescriptions = {
'url': 'The URL to make the request to',
'method': 'HTTP method to use for the request',
'authentication': 'Authentication method to use',
'sendbody': 'Whether to send a request body',
'contenttype': 'Content type of the request body',
'sendheaders': 'Whether to send custom headers',
'jsonbody': 'JSON data to send in the request body',
'headers': 'Custom headers to send with the request',
'timeout': 'Request timeout in milliseconds',
'query': 'SQL query to execute',
'table': 'Database table name',
'operation': 'Operation to perform',
'path': 'Webhook path or file path',
'httpmethod': 'HTTP method to accept',
'responsemode': 'How to respond to the webhook',
'responsecode': 'HTTP response code to return',
'channel': 'Slack channel to send message to',
'text': 'Text content of the message',
'subject': 'Email subject line',
'fromemail': 'Sender email address',
'toemail': 'Recipient email address',
'language': 'Programming language to use',
'jscode': 'JavaScript code to execute',
'pythoncode': 'Python code to execute'
};
if (commonDescriptions[name]) {
return commonDescriptions[name];
}
for (const [key, desc] of Object.entries(commonDescriptions)) {
if (name.includes(key)) {
return desc;
}
}
if (type === 'boolean') {
return `Enable or disable ${prop.displayName || name}`;
}
else if (type === 'options') {
return `Select ${prop.displayName || name}`;
}
else if (type === 'string') {
return `Enter ${prop.displayName || name}`;
}
else if (type === 'number') {
return `Number value for ${prop.displayName || name}`;
}
else if (type === 'json') {
return `JSON data for ${prop.displayName || name}`;
}
return `Configure ${prop.displayName || name}`;
}
static inferEssentials(properties) {
const required = properties
.filter(p => p.name && p.required === true)
.slice(0, 10)
.map(p => this.simplifyProperty(p));
const common = properties
.filter(p => {
return p.name &&
!p.required &&
!p.displayOptions &&
p.type !== 'hidden' &&
p.type !== 'notice' &&
!p.name.startsWith('options') &&
!p.name.startsWith('_');
})
.slice(0, 10)
.map(p => this.simplifyProperty(p));
if (required.length + common.length < 10) {
const additional = properties
.filter(p => {
return p.name &&
!p.required &&
p.type !== 'hidden' &&
p.displayOptions &&
Object.keys(p.displayOptions.show || {}).length === 1;
})
.slice(0, 10 - (required.length + common.length))
.map(p => this.simplifyProperty(p));
common.push(...additional);
}
const totalLimit = 30;
if (required.length + common.length > totalLimit) {
const requiredCount = Math.min(required.length, 15);
const commonCount = totalLimit - requiredCount;
return {
required: required.slice(0, requiredCount),
common: common.slice(0, commonCount)
};
}
return { required, common };
}
static searchProperties(allProperties, query, maxResults = 20) {
if (!query || query.trim() === '') {
return [];
}
const lowerQuery = query.toLowerCase();
const matches = [];
this.searchPropertiesRecursive(allProperties, lowerQuery, matches);
return matches
.sort((a, b) => b.score - a.score)
.slice(0, maxResults)
.map(match => ({
...this.simplifyProperty(match.property),
path: match.path
}));
}
static searchPropertiesRecursive(properties, query, matches, path = '') {
for (const prop of properties) {
const currentPath = path ? `${path}.${prop.name}` : prop.name;
let score = 0;
if (prop.name.toLowerCase() === query) {
score = 10;
}
else if (prop.name.toLowerCase().startsWith(query)) {
score = 8;
}
else if (prop.name.toLowerCase().includes(query)) {
score = 5;
}
if (prop.displayName?.toLowerCase().includes(query)) {
score = Math.max(score, 4);
}
if (prop.description?.toLowerCase().includes(query)) {
score = Math.max(score, 3);
}
if (score > 0) {
matches.push({ property: prop, score, path: currentPath });
}
if (prop.type === 'collection' && prop.options) {
this.searchPropertiesRecursive(prop.options, query, matches, currentPath);
}
else if (prop.type === 'fixedCollection' && prop.options) {
for (const option of prop.options) {
if (option.values) {
this.searchPropertiesRecursive(option.values, query, matches, `${currentPath}.${option.name}`);
}
}
}
}
}
}
exports.PropertyFilter = PropertyFilter;
PropertyFilter.ESSENTIAL_PROPERTIES = {
'nodes-base.httpRequest': {
required: ['url'],
common: ['method', 'authentication', 'sendBody', 'contentType', 'sendHeaders'],
categoryPriority: ['basic', 'authentication', 'request', 'response', 'advanced']
},
'nodes-base.webhook': {
required: [],
common: ['httpMethod', 'path', 'responseMode', 'responseData', 'responseCode'],
categoryPriority: ['basic', 'response', 'advanced']
},
'nodes-base.code': {
required: [],
common: ['language', 'jsCode', 'pythonCode', 'mode'],
categoryPriority: ['basic', 'code', 'advanced']
},
'nodes-base.set': {
required: [],
common: ['mode', 'assignments', 'includeOtherFields', 'options'],
categoryPriority: ['basic', 'data', 'advanced']
},
'nodes-base.if': {
required: [],
common: ['conditions', 'combineOperation'],
categoryPriority: ['basic', 'conditions', 'advanced']
},
'nodes-base.postgres': {
required: [],
common: ['operation', 'table', 'query', 'additionalFields', 'returnAll'],
categoryPriority: ['basic', 'query', 'options', 'advanced']
},
'nodes-base.openAi': {
required: [],
common: ['resource', 'operation', 'modelId', 'prompt', 'messages', 'maxTokens'],
categoryPriority: ['basic', 'model', 'input', 'options', 'advanced']
},
'nodes-base.googleSheets': {
required: [],
common: ['operation', 'documentId', 'sheetName', 'range', 'dataStartRow'],
categoryPriority: ['basic', 'location', 'data', 'options', 'advanced']
},
'nodes-base.slack': {
required: [],
common: ['resource', 'operation', 'channel', 'text', 'attachments', 'blocks'],
categoryPriority: ['basic', 'message', 'formatting', 'advanced']
},
'nodes-base.email': {
required: [],
common: ['resource', 'operation', 'fromEmail', 'toEmail', 'subject', 'text', 'html'],
categoryPriority: ['basic', 'recipients', 'content', 'advanced']
},
'nodes-base.merge': {
required: [],
common: ['mode', 'joinMode', 'propertyName1', 'propertyName2', 'outputDataFrom'],
categoryPriority: ['basic', 'merge', 'advanced']
},
'nodes-base.function': {
required: [],
common: ['functionCode'],
categoryPriority: ['basic', 'code', 'advanced']
},
'nodes-base.splitInBatches': {
required: [],
common: ['batchSize', 'options'],
categoryPriority: ['basic', 'options', 'advanced']
},
'nodes-base.redis': {
required: [],
common: ['operation', 'key', 'value', 'keyType', 'expire'],
categoryPriority: ['basic', 'data', 'options', 'advanced']
},
'nodes-base.mongoDb': {
required: [],
common: ['operation', 'collection', 'query', 'fields', 'limit'],
categoryPriority: ['basic', 'query', 'options', 'advanced']
},
'nodes-base.mySql': {
required: [],
common: ['operation', 'table', 'query', 'columns', 'additionalFields'],
categoryPriority: ['basic', 'query', 'options', 'advanced']
},
'nodes-base.ftp': {
required: [],
common: ['operation', 'path', 'fileName', 'binaryData'],
categoryPriority: ['basic', 'file', 'options', 'advanced']
},
'nodes-base.ssh': {
required: [],
common: ['resource', 'operation', 'command', 'path', 'cwd'],
categoryPriority: ['basic', 'command', 'options', 'advanced']
},
'nodes-base.executeCommand': {
required: [],
common: ['command', 'cwd'],
categoryPriority: ['basic', 'advanced']
},
'nodes-base.github': {
required: [],
common: ['resource', 'operation', 'owner', 'repository', 'title', 'body'],
categoryPriority: ['basic', 'repository', 'content', 'advanced']
}
};
//# sourceMappingURL=property-filter.js.map

1
dist/services/property-filter.js.map vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,33 @@
import { NodeRepository } from '../database/node-repository';
export interface ResourceSuggestion {
value: string;
confidence: number;
reason: string;
availableOperations?: string[];
}
export declare class ResourceSimilarityService {
private static readonly CACHE_DURATION_MS;
private static readonly MIN_CONFIDENCE;
private static readonly MAX_SUGGESTIONS;
private static readonly CONFIDENCE_THRESHOLDS;
private repository;
private resourceCache;
private suggestionCache;
private commonPatterns;
constructor(repository: NodeRepository);
private cleanupExpiredEntries;
private initializeCommonPatterns;
findSimilarResources(nodeType: string, invalidResource: string, maxSuggestions?: number): ResourceSuggestion[];
private getResourceValue;
private getNodeResources;
private extractImplicitResources;
private inferResourceFromOperations;
private getNodePatterns;
private toSingular;
private toPlural;
private calculateSimilarity;
private levenshteinDistance;
private getSimilarityReason;
clearCache(): void;
}
//# sourceMappingURL=resource-similarity-service.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"resource-similarity-service.d.ts","sourceRoot":"","sources":["../../src/services/resource-similarity-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAI7D,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;CAChC;AASD,qBAAa,yBAAyB;IACpC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAiB;IAC1D,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAO;IAC7C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAK;IAG5C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAMlC;IAEX,OAAO,CAAC,UAAU,CAAiB;IACnC,OAAO,CAAC,aAAa,CAAmE;IACxF,OAAO,CAAC,eAAe,CAAgD;IACvE,OAAO,CAAC,cAAc,CAAiC;gBAE3C,UAAU,EAAE,cAAc;IAQtC,OAAO,CAAC,qBAAqB;IAwB7B,OAAO,CAAC,wBAAwB;IA2EhC,oBAAoB,CAClB,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,MAAM,EACvB,cAAc,GAAE,MAAkD,GACjE,kBAAkB,EAAE;IA6FvB,OAAO,CAAC,gBAAgB;IAaxB,OAAO,CAAC,gBAAgB;IA0ExB,OAAO,CAAC,wBAAwB;IAwBhC,OAAO,CAAC,2BAA2B;IA2BnC,OAAO,CAAC,eAAe;IAyBvB,OAAO,CAAC,UAAU;IAclB,OAAO,CAAC,QAAQ;IAchB,OAAO,CAAC,mBAAmB;IAkC3B,OAAO,CAAC,mBAAmB;IAgC3B,OAAO,CAAC,mBAAmB;IAmB3B,UAAU,IAAI,IAAI;CAInB"}

View File

@@ -0,0 +1,358 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ResourceSimilarityService = void 0;
const logger_1 = require("../utils/logger");
class ResourceSimilarityService {
constructor(repository) {
this.resourceCache = new Map();
this.suggestionCache = new Map();
this.repository = repository;
this.commonPatterns = this.initializeCommonPatterns();
}
cleanupExpiredEntries() {
const now = Date.now();
for (const [key, value] of this.resourceCache.entries()) {
if (now - value.timestamp >= ResourceSimilarityService.CACHE_DURATION_MS) {
this.resourceCache.delete(key);
}
}
if (this.suggestionCache.size > 100) {
const entries = Array.from(this.suggestionCache.entries());
this.suggestionCache.clear();
entries.slice(-50).forEach(([key, value]) => {
this.suggestionCache.set(key, value);
});
}
}
initializeCommonPatterns() {
const patterns = new Map();
patterns.set('googleDrive', [
{ pattern: 'files', suggestion: 'file', confidence: 0.95, reason: 'Use singular "file" not plural' },
{ pattern: 'folders', suggestion: 'folder', confidence: 0.95, reason: 'Use singular "folder" not plural' },
{ pattern: 'permissions', suggestion: 'permission', confidence: 0.9, reason: 'Use singular form' },
{ pattern: 'fileAndFolder', suggestion: 'fileFolder', confidence: 0.9, reason: 'Use "fileFolder" for combined operations' },
{ pattern: 'driveFiles', suggestion: 'file', confidence: 0.8, reason: 'Use "file" for file operations' },
{ pattern: 'sharedDrives', suggestion: 'drive', confidence: 0.85, reason: 'Use "drive" for shared drive operations' },
]);
patterns.set('slack', [
{ pattern: 'messages', suggestion: 'message', confidence: 0.95, reason: 'Use singular "message" not plural' },
{ pattern: 'channels', suggestion: 'channel', confidence: 0.95, reason: 'Use singular "channel" not plural' },
{ pattern: 'users', suggestion: 'user', confidence: 0.95, reason: 'Use singular "user" not plural' },
{ pattern: 'msg', suggestion: 'message', confidence: 0.85, reason: 'Use full "message" not abbreviation' },
{ pattern: 'dm', suggestion: 'message', confidence: 0.7, reason: 'Use "message" for direct messages' },
{ pattern: 'conversation', suggestion: 'channel', confidence: 0.7, reason: 'Use "channel" for conversations' },
]);
patterns.set('database', [
{ pattern: 'tables', suggestion: 'table', confidence: 0.95, reason: 'Use singular "table" not plural' },
{ pattern: 'queries', suggestion: 'query', confidence: 0.95, reason: 'Use singular "query" not plural' },
{ pattern: 'collections', suggestion: 'collection', confidence: 0.95, reason: 'Use singular "collection" not plural' },
{ pattern: 'documents', suggestion: 'document', confidence: 0.95, reason: 'Use singular "document" not plural' },
{ pattern: 'records', suggestion: 'record', confidence: 0.85, reason: 'Use "record" or "document"' },
{ pattern: 'rows', suggestion: 'row', confidence: 0.9, reason: 'Use singular "row"' },
]);
patterns.set('googleSheets', [
{ pattern: 'sheets', suggestion: 'sheet', confidence: 0.95, reason: 'Use singular "sheet" not plural' },
{ pattern: 'spreadsheets', suggestion: 'spreadsheet', confidence: 0.95, reason: 'Use singular "spreadsheet"' },
{ pattern: 'cells', suggestion: 'cell', confidence: 0.9, reason: 'Use singular "cell"' },
{ pattern: 'ranges', suggestion: 'range', confidence: 0.9, reason: 'Use singular "range"' },
{ pattern: 'worksheets', suggestion: 'sheet', confidence: 0.8, reason: 'Use "sheet" for worksheet operations' },
]);
patterns.set('email', [
{ pattern: 'emails', suggestion: 'email', confidence: 0.95, reason: 'Use singular "email" not plural' },
{ pattern: 'messages', suggestion: 'message', confidence: 0.9, reason: 'Use "message" for email operations' },
{ pattern: 'mails', suggestion: 'email', confidence: 0.9, reason: 'Use "email" not "mail"' },
{ pattern: 'attachments', suggestion: 'attachment', confidence: 0.95, reason: 'Use singular "attachment"' },
]);
patterns.set('generic', [
{ pattern: 'items', suggestion: 'item', confidence: 0.9, reason: 'Use singular form' },
{ pattern: 'objects', suggestion: 'object', confidence: 0.9, reason: 'Use singular form' },
{ pattern: 'entities', suggestion: 'entity', confidence: 0.9, reason: 'Use singular form' },
{ pattern: 'resources', suggestion: 'resource', confidence: 0.9, reason: 'Use singular form' },
{ pattern: 'elements', suggestion: 'element', confidence: 0.9, reason: 'Use singular form' },
]);
return patterns;
}
findSimilarResources(nodeType, invalidResource, maxSuggestions = ResourceSimilarityService.MAX_SUGGESTIONS) {
if (Math.random() < 0.1) {
this.cleanupExpiredEntries();
}
const cacheKey = `${nodeType}:${invalidResource}`;
if (this.suggestionCache.has(cacheKey)) {
return this.suggestionCache.get(cacheKey);
}
const suggestions = [];
const validResources = this.getNodeResources(nodeType);
for (const resource of validResources) {
const resourceValue = this.getResourceValue(resource);
if (resourceValue.toLowerCase() === invalidResource.toLowerCase()) {
return [];
}
}
const nodePatterns = this.getNodePatterns(nodeType);
for (const pattern of nodePatterns) {
if (pattern.pattern.toLowerCase() === invalidResource.toLowerCase()) {
const exists = validResources.some(r => {
const resourceValue = this.getResourceValue(r);
return resourceValue === pattern.suggestion;
});
if (exists) {
suggestions.push({
value: pattern.suggestion,
confidence: pattern.confidence,
reason: pattern.reason
});
}
}
}
const singularForm = this.toSingular(invalidResource);
const pluralForm = this.toPlural(invalidResource);
for (const resource of validResources) {
const resourceValue = this.getResourceValue(resource);
if (resourceValue === singularForm || resourceValue === pluralForm) {
if (!suggestions.some(s => s.value === resourceValue)) {
suggestions.push({
value: resourceValue,
confidence: 0.9,
reason: invalidResource.endsWith('s') ?
'Use singular form for resources' :
'Incorrect plural/singular form',
availableOperations: typeof resource === 'object' ? resource.operations : undefined
});
}
}
const similarity = this.calculateSimilarity(invalidResource, resourceValue);
if (similarity >= ResourceSimilarityService.MIN_CONFIDENCE) {
if (!suggestions.some(s => s.value === resourceValue)) {
suggestions.push({
value: resourceValue,
confidence: similarity,
reason: this.getSimilarityReason(similarity, invalidResource, resourceValue),
availableOperations: typeof resource === 'object' ? resource.operations : undefined
});
}
}
}
suggestions.sort((a, b) => b.confidence - a.confidence);
const topSuggestions = suggestions.slice(0, maxSuggestions);
this.suggestionCache.set(cacheKey, topSuggestions);
return topSuggestions;
}
getResourceValue(resource) {
if (typeof resource === 'string') {
return resource;
}
if (typeof resource === 'object' && resource !== null) {
return resource.value || '';
}
return '';
}
getNodeResources(nodeType) {
if (Math.random() < 0.05) {
this.cleanupExpiredEntries();
}
const cacheKey = nodeType;
const cached = this.resourceCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < ResourceSimilarityService.CACHE_DURATION_MS) {
return cached.resources;
}
const nodeInfo = this.repository.getNode(nodeType);
if (!nodeInfo)
return [];
const resources = [];
const resourceMap = new Map();
try {
const properties = nodeInfo.properties || [];
for (const prop of properties) {
if (prop.name === 'resource' && prop.options) {
for (const option of prop.options) {
resources.push({
value: option.value,
name: option.name,
operations: []
});
resourceMap.set(option.value, []);
}
}
if (prop.name === 'operation' && prop.displayOptions?.show?.resource) {
const resourceValues = Array.isArray(prop.displayOptions.show.resource)
? prop.displayOptions.show.resource
: [prop.displayOptions.show.resource];
for (const resourceValue of resourceValues) {
if (resourceMap.has(resourceValue) && prop.options) {
const ops = prop.options.map((op) => op.value);
resourceMap.get(resourceValue).push(...ops);
}
}
}
}
for (const resource of resources) {
if (resourceMap.has(resource.value)) {
resource.operations = resourceMap.get(resource.value);
}
}
if (resources.length === 0) {
const implicitResources = this.extractImplicitResources(properties);
resources.push(...implicitResources);
}
}
catch (error) {
logger_1.logger.warn(`Failed to extract resources for ${nodeType}:`, error);
}
this.resourceCache.set(cacheKey, { resources, timestamp: Date.now() });
return resources;
}
extractImplicitResources(properties) {
const resources = [];
for (const prop of properties) {
if (prop.name === 'operation' && prop.options) {
const resourceFromOps = this.inferResourceFromOperations(prop.options);
if (resourceFromOps) {
resources.push({
value: resourceFromOps,
name: resourceFromOps.charAt(0).toUpperCase() + resourceFromOps.slice(1),
operations: prop.options.map((op) => op.value)
});
}
}
}
return resources;
}
inferResourceFromOperations(operations) {
const patterns = [
{ keywords: ['file', 'upload', 'download'], resource: 'file' },
{ keywords: ['folder', 'directory'], resource: 'folder' },
{ keywords: ['message', 'send', 'reply'], resource: 'message' },
{ keywords: ['channel', 'broadcast'], resource: 'channel' },
{ keywords: ['user', 'member'], resource: 'user' },
{ keywords: ['table', 'row', 'column'], resource: 'table' },
{ keywords: ['document', 'doc'], resource: 'document' },
];
for (const pattern of patterns) {
for (const op of operations) {
const opName = (op.value || op).toLowerCase();
if (pattern.keywords.some(keyword => opName.includes(keyword))) {
return pattern.resource;
}
}
}
return null;
}
getNodePatterns(nodeType) {
const patterns = [];
if (nodeType.includes('googleDrive')) {
patterns.push(...(this.commonPatterns.get('googleDrive') || []));
}
else if (nodeType.includes('slack')) {
patterns.push(...(this.commonPatterns.get('slack') || []));
}
else if (nodeType.includes('postgres') || nodeType.includes('mysql') || nodeType.includes('mongodb')) {
patterns.push(...(this.commonPatterns.get('database') || []));
}
else if (nodeType.includes('googleSheets')) {
patterns.push(...(this.commonPatterns.get('googleSheets') || []));
}
else if (nodeType.includes('gmail') || nodeType.includes('email')) {
patterns.push(...(this.commonPatterns.get('email') || []));
}
patterns.push(...(this.commonPatterns.get('generic') || []));
return patterns;
}
toSingular(word) {
if (word.endsWith('ies')) {
return word.slice(0, -3) + 'y';
}
else if (word.endsWith('es')) {
return word.slice(0, -2);
}
else if (word.endsWith('s') && !word.endsWith('ss')) {
return word.slice(0, -1);
}
return word;
}
toPlural(word) {
if (word.endsWith('y') && !['ay', 'ey', 'iy', 'oy', 'uy'].includes(word.slice(-2))) {
return word.slice(0, -1) + 'ies';
}
else if (word.endsWith('s') || word.endsWith('x') || word.endsWith('z') ||
word.endsWith('ch') || word.endsWith('sh')) {
return word + 'es';
}
else {
return word + 's';
}
}
calculateSimilarity(str1, str2) {
const s1 = str1.toLowerCase();
const s2 = str2.toLowerCase();
if (s1 === s2)
return 1.0;
if (s1.includes(s2) || s2.includes(s1)) {
const ratio = Math.min(s1.length, s2.length) / Math.max(s1.length, s2.length);
return Math.max(ResourceSimilarityService.CONFIDENCE_THRESHOLDS.MIN_SUBSTRING, ratio);
}
const distance = this.levenshteinDistance(s1, s2);
const maxLength = Math.max(s1.length, s2.length);
let similarity = 1 - (distance / maxLength);
if (distance === 1 && maxLength <= 5) {
similarity = Math.max(similarity, 0.75);
}
else if (distance === 2 && maxLength <= 5) {
similarity = Math.max(similarity, 0.72);
}
return similarity;
}
levenshteinDistance(str1, str2) {
const m = str1.length;
const n = str2.length;
const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
for (let i = 0; i <= m; i++)
dp[i][0] = i;
for (let j = 0; j <= n; j++)
dp[0][j] = j;
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (str1[i - 1] === str2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
}
else {
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 1);
}
}
}
return dp[m][n];
}
getSimilarityReason(confidence, invalid, valid) {
const { VERY_HIGH, HIGH, MEDIUM } = ResourceSimilarityService.CONFIDENCE_THRESHOLDS;
if (confidence >= VERY_HIGH) {
return 'Almost exact match - likely a typo';
}
else if (confidence >= HIGH) {
return 'Very similar - common variation';
}
else if (confidence >= MEDIUM) {
return 'Similar resource name';
}
else if (invalid.includes(valid) || valid.includes(invalid)) {
return 'Partial match';
}
else {
return 'Possibly related resource';
}
}
clearCache() {
this.resourceCache.clear();
this.suggestionCache.clear();
}
}
exports.ResourceSimilarityService = ResourceSimilarityService;
ResourceSimilarityService.CACHE_DURATION_MS = 5 * 60 * 1000;
ResourceSimilarityService.MIN_CONFIDENCE = 0.3;
ResourceSimilarityService.MAX_SUGGESTIONS = 5;
ResourceSimilarityService.CONFIDENCE_THRESHOLDS = {
EXACT: 1.0,
VERY_HIGH: 0.95,
HIGH: 0.8,
MEDIUM: 0.6,
MIN_SUBSTRING: 0.7
};
//# sourceMappingURL=resource-similarity-service.js.map

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More