UNPKG

n8n-mcp

Version:

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

223 lines 10.1 kB
"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 checkCachedResultName(value, path) { if (!this.MODES_USING_CACHED_NAME.includes(value.mode)) { return null; } if (typeof value.cachedResultName === 'string' && value.cachedResultName !== '') { return null; } return { fieldPath: path, currentValue: value, correctedValue: { ...value, cachedResultName: '<set to the resource display name>' }, issueType: 'missing-cached-result-name', explanation: 'resource locator is missing cachedResultName. The workflow will run, but the n8n UI dropdown will show "Choose..." instead of the selected value, and dependent metadata fetches (e.g. column lists) will not fire. Set cachedResultName to the human-readable display name of the resource. (#715)', severity: 'warning' }; } 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' }; } return null; } static validateNodeParameters(parameters, context, profile) { const issues = []; const visited = new WeakSet(); this.validateRecursive(parameters, '', context, issues, visited); if (profile === 'minimal' || profile === 'runtime') { return issues.filter(i => i.issueType !== 'missing-cached-result-name'); } 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)) { const cachedNameIssue = this.checkCachedResultName(obj, path); if (cachedNameIssue) issues.push(cachedNameIssue); return; } Object.entries(obj).forEach(([key, value]) => { if (key.startsWith('__')) return; if (key === 'jsCode' || key === 'pythonCode' || key === 'functionCode') return; if (/\[\d+\]/.test(key)) 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`; } const fixedLabel = issue.issueType === 'missing-cached-result-name' ? 'Suggested shape (replace the placeholder with the actual resource display name):' : 'Fixed (correct):'; message += `${fixedLabel}\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'], }; ExpressionFormatValidator.MODES_USING_CACHED_NAME = ['id', 'list', 'name']; //# sourceMappingURL=expression-format-validator.js.map