mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-04-05 00:53:07 +00:00
chore: add pre-built dist folder for npx usage
This commit is contained in:
committed by
Romuald Członkowski
parent
a70d96a373
commit
5057481e70
12
dist/services/ai-node-validator.d.ts
vendored
Normal file
12
dist/services/ai-node-validator.d.ts
vendored
Normal 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
|
||||
1
dist/services/ai-node-validator.d.ts.map
vendored
Normal file
1
dist/services/ai-node-validator.d.ts.map
vendored
Normal 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
429
dist/services/ai-node-validator.js
vendored
Normal 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
|
||||
1
dist/services/ai-node-validator.js.map
vendored
Normal file
1
dist/services/ai-node-validator.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
58
dist/services/ai-tool-validators.d.ts
vendored
Normal file
58
dist/services/ai-tool-validators.d.ts
vendored
Normal 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
|
||||
1
dist/services/ai-tool-validators.d.ts.map
vendored
Normal file
1
dist/services/ai-tool-validators.d.ts.map
vendored
Normal 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
438
dist/services/ai-tool-validators.js
vendored
Normal 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
|
||||
1
dist/services/ai-tool-validators.js.map
vendored
Normal file
1
dist/services/ai-tool-validators.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
38
dist/services/breaking-change-detector.d.ts
vendored
Normal file
38
dist/services/breaking-change-detector.d.ts
vendored
Normal 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
|
||||
1
dist/services/breaking-change-detector.d.ts.map
vendored
Normal file
1
dist/services/breaking-change-detector.d.ts.map
vendored
Normal 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"}
|
||||
184
dist/services/breaking-change-detector.js
vendored
Normal file
184
dist/services/breaking-change-detector.js
vendored
Normal 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
|
||||
1
dist/services/breaking-change-detector.js.map
vendored
Normal file
1
dist/services/breaking-change-detector.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
28
dist/services/breaking-changes-registry.d.ts
vendored
Normal file
28
dist/services/breaking-changes-registry.d.ts
vendored
Normal 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
|
||||
1
dist/services/breaking-changes-registry.d.ts.map
vendored
Normal file
1
dist/services/breaking-changes-registry.d.ts.map
vendored
Normal 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"}
|
||||
200
dist/services/breaking-changes-registry.js
vendored
Normal file
200
dist/services/breaking-changes-registry.js
vendored
Normal 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
|
||||
1
dist/services/breaking-changes-registry.js.map
vendored
Normal file
1
dist/services/breaking-changes-registry.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
24
dist/services/confidence-scorer.d.ts
vendored
Normal file
24
dist/services/confidence-scorer.d.ts
vendored
Normal 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
|
||||
1
dist/services/confidence-scorer.d.ts.map
vendored
Normal file
1
dist/services/confidence-scorer.d.ts.map
vendored
Normal 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
139
dist/services/confidence-scorer.js
vendored
Normal 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
|
||||
1
dist/services/confidence-scorer.js.map
vendored
Normal file
1
dist/services/confidence-scorer.js.map
vendored
Normal 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
47
dist/services/config-validator.d.ts
vendored
Normal 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
|
||||
1
dist/services/config-validator.d.ts.map
vendored
Normal file
1
dist/services/config-validator.d.ts.map
vendored
Normal 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
671
dist/services/config-validator.js
vendored
Normal 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
1
dist/services/config-validator.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
54
dist/services/enhanced-config-validator.d.ts
vendored
Normal file
54
dist/services/enhanced-config-validator.d.ts
vendored
Normal 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
|
||||
1
dist/services/enhanced-config-validator.d.ts.map
vendored
Normal file
1
dist/services/enhanced-config-validator.d.ts.map
vendored
Normal 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"}
|
||||
789
dist/services/enhanced-config-validator.js
vendored
Normal file
789
dist/services/enhanced-config-validator.js
vendored
Normal 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
|
||||
1
dist/services/enhanced-config-validator.js.map
vendored
Normal file
1
dist/services/enhanced-config-validator.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
14
dist/services/example-generator.d.ts
vendored
Normal file
14
dist/services/example-generator.d.ts
vendored
Normal 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
|
||||
1
dist/services/example-generator.d.ts.map
vendored
Normal file
1
dist/services/example-generator.d.ts.map
vendored
Normal 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
970
dist/services/example-generator.js
vendored
Normal 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
|
||||
1
dist/services/example-generator.js.map
vendored
Normal file
1
dist/services/example-generator.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
8
dist/services/execution-processor.d.ts
vendored
Normal file
8
dist/services/execution-processor.d.ts
vendored
Normal 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
|
||||
1
dist/services/execution-processor.d.ts.map
vendored
Normal file
1
dist/services/execution-processor.d.ts.map
vendored
Normal 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
359
dist/services/execution-processor.js
vendored
Normal 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
|
||||
1
dist/services/execution-processor.js.map
vendored
Normal file
1
dist/services/execution-processor.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
33
dist/services/expression-format-validator.d.ts
vendored
Normal file
33
dist/services/expression-format-validator.d.ts
vendored
Normal 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
|
||||
1
dist/services/expression-format-validator.d.ts.map
vendored
Normal file
1
dist/services/expression-format-validator.d.ts.map
vendored
Normal 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"}
|
||||
209
dist/services/expression-format-validator.js
vendored
Normal file
209
dist/services/expression-format-validator.js
vendored
Normal 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
|
||||
1
dist/services/expression-format-validator.js.map
vendored
Normal file
1
dist/services/expression-format-validator.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
27
dist/services/expression-validator.d.ts
vendored
Normal file
27
dist/services/expression-validator.d.ts
vendored
Normal 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
|
||||
1
dist/services/expression-validator.d.ts.map
vendored
Normal file
1
dist/services/expression-validator.d.ts.map
vendored
Normal 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
187
dist/services/expression-validator.js
vendored
Normal 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
|
||||
1
dist/services/expression-validator.js.map
vendored
Normal file
1
dist/services/expression-validator.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
47
dist/services/n8n-api-client.d.ts
vendored
Normal file
47
dist/services/n8n-api-client.d.ts
vendored
Normal 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
1
dist/services/n8n-api-client.d.ts.map
vendored
Normal 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
445
dist/services/n8n-api-client.js
vendored
Normal 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
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
273
dist/services/n8n-validation.d.ts
vendored
Normal 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
1
dist/services/n8n-validation.d.ts.map
vendored
Normal 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
481
dist/services/n8n-validation.js
vendored
Normal 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
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
23
dist/services/n8n-version.d.ts
vendored
Normal 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
1
dist/services/n8n-version.d.ts.map
vendored
Normal 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
142
dist/services/n8n-version.js
vendored
Normal 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
1
dist/services/n8n-version.js.map
vendored
Normal 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"}
|
||||
70
dist/services/node-documentation-service.d.ts
vendored
Normal file
70
dist/services/node-documentation-service.d.ts
vendored
Normal 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
|
||||
1
dist/services/node-documentation-service.d.ts.map
vendored
Normal file
1
dist/services/node-documentation-service.d.ts.map
vendored
Normal 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"}
|
||||
518
dist/services/node-documentation-service.js
vendored
Normal file
518
dist/services/node-documentation-service.js
vendored
Normal 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
|
||||
1
dist/services/node-documentation-service.js.map
vendored
Normal file
1
dist/services/node-documentation-service.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
44
dist/services/node-migration-service.d.ts
vendored
Normal file
44
dist/services/node-migration-service.d.ts
vendored
Normal 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
|
||||
1
dist/services/node-migration-service.d.ts.map
vendored
Normal file
1
dist/services/node-migration-service.d.ts.map
vendored
Normal 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
231
dist/services/node-migration-service.js
vendored
Normal 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
|
||||
1
dist/services/node-migration-service.js.map
vendored
Normal file
1
dist/services/node-migration-service.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
5
dist/services/node-sanitizer.d.ts
vendored
Normal file
5
dist/services/node-sanitizer.d.ts
vendored
Normal 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
1
dist/services/node-sanitizer.d.ts.map
vendored
Normal 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
225
dist/services/node-sanitizer.js
vendored
Normal 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
1
dist/services/node-sanitizer.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
51
dist/services/node-similarity-service.d.ts
vendored
Normal file
51
dist/services/node-similarity-service.d.ts
vendored
Normal 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
|
||||
1
dist/services/node-similarity-service.d.ts.map
vendored
Normal file
1
dist/services/node-similarity-service.d.ts.map
vendored
Normal 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
335
dist/services/node-similarity-service.js
vendored
Normal 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
|
||||
1
dist/services/node-similarity-service.js.map
vendored
Normal file
1
dist/services/node-similarity-service.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
37
dist/services/node-specific-validators.d.ts
vendored
Normal file
37
dist/services/node-specific-validators.d.ts
vendored
Normal 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
|
||||
1
dist/services/node-specific-validators.d.ts.map
vendored
Normal file
1
dist/services/node-specific-validators.d.ts.map
vendored
Normal 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
1331
dist/services/node-specific-validators.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
dist/services/node-specific-validators.js.map
vendored
Normal file
1
dist/services/node-specific-validators.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
63
dist/services/node-version-service.d.ts
vendored
Normal file
63
dist/services/node-version-service.d.ts
vendored
Normal 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
|
||||
1
dist/services/node-version-service.d.ts.map
vendored
Normal file
1
dist/services/node-version-service.d.ts.map
vendored
Normal 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
215
dist/services/node-version-service.js
vendored
Normal 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
|
||||
1
dist/services/node-version-service.js.map
vendored
Normal file
1
dist/services/node-version-service.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
32
dist/services/operation-similarity-service.d.ts
vendored
Normal file
32
dist/services/operation-similarity-service.d.ts
vendored
Normal 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
|
||||
1
dist/services/operation-similarity-service.d.ts.map
vendored
Normal file
1
dist/services/operation-similarity-service.d.ts.map
vendored
Normal 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"}
|
||||
341
dist/services/operation-similarity-service.js
vendored
Normal file
341
dist/services/operation-similarity-service.js
vendored
Normal 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
|
||||
1
dist/services/operation-similarity-service.js.map
vendored
Normal file
1
dist/services/operation-similarity-service.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
59
dist/services/post-update-validator.d.ts
vendored
Normal file
59
dist/services/post-update-validator.d.ts
vendored
Normal 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
|
||||
1
dist/services/post-update-validator.d.ts.map
vendored
Normal file
1
dist/services/post-update-validator.d.ts.map
vendored
Normal 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
231
dist/services/post-update-validator.js
vendored
Normal 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
|
||||
1
dist/services/post-update-validator.js.map
vendored
Normal file
1
dist/services/post-update-validator.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
36
dist/services/property-dependencies.d.ts
vendored
Normal file
36
dist/services/property-dependencies.d.ts
vendored
Normal 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
|
||||
1
dist/services/property-dependencies.d.ts.map
vendored
Normal file
1
dist/services/property-dependencies.d.ts.map
vendored
Normal 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
168
dist/services/property-dependencies.js
vendored
Normal 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
|
||||
1
dist/services/property-dependencies.js.map
vendored
Normal file
1
dist/services/property-dependencies.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
44
dist/services/property-filter.d.ts
vendored
Normal file
44
dist/services/property-filter.d.ts
vendored
Normal 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
|
||||
1
dist/services/property-filter.d.ts.map
vendored
Normal file
1
dist/services/property-filter.d.ts.map
vendored
Normal 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
395
dist/services/property-filter.js
vendored
Normal 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
1
dist/services/property-filter.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
33
dist/services/resource-similarity-service.d.ts
vendored
Normal file
33
dist/services/resource-similarity-service.d.ts
vendored
Normal 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
|
||||
1
dist/services/resource-similarity-service.d.ts.map
vendored
Normal file
1
dist/services/resource-similarity-service.d.ts.map
vendored
Normal 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"}
|
||||
358
dist/services/resource-similarity-service.js
vendored
Normal file
358
dist/services/resource-similarity-service.js
vendored
Normal 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
|
||||
1
dist/services/resource-similarity-service.js.map
vendored
Normal file
1
dist/services/resource-similarity-service.js.map
vendored
Normal file
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
Reference in New Issue
Block a user