n8n-mcp
Version:
Integration between n8n workflow automation and Model Context Protocol (MCP)
347 lines • 16.2 kB
JavaScript
;
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