UNPKG

n8n-mcp

Version:

Integration between n8n workflow automation and Model Context Protocol (MCP)

347 lines 16.2 kB
"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"); class EnhancedConfigValidator extends config_validator_1.ConfigValidator { 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 filteredProperties = this.filterPropertiesByMode(properties, config, mode, operationContext); const baseResult = super.validate(nodeType, config, filteredProperties); 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, 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) { switch (mode) { case 'minimal': return properties.filter(prop => prop.required && this.isPropertyVisible(prop, config)); case 'operation': return properties.filter(prop => this.isPropertyRelevantToOperation(prop, config, operation)); case 'full': default: return properties; } } 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, 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.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-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) { } 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 applyProfileFilters(result, profile) { switch (profile) { case 'minimal': result.errors = result.errors.filter(e => e.type === 'missing_required'); result.warnings = []; 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 => w.type === 'security'); 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 => w.type !== 'inefficient' || !w.property?.startsWith('_')); 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; } } exports.EnhancedConfigValidator = EnhancedConfigValidator; //# sourceMappingURL=enhanced-config-validator.js.map