mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-03-28 05:03:07 +00:00
Three critical fixes in n8n_update_partial_workflow: - **#665**: Replace incorrect `isNotEmpty`/`isEmpty` operator names with `notEmpty`/`empty` across validators, sanitizer, docs, and error messages. Add auto-correction in sanitizer. Unknown operators silently returned false in n8n's execution engine. - **#659**: Remap numeric `targetInput` values (e.g., "0") to "main" in addConnection. Relax sourceOutput remapping guard for redundant sourceOutput+sourceIndex combinations. Also resolves #653 (dangling connections caused by malformed type:"0" connections). - **#642**: Implement __patch_find_replace for surgical string edits in updateNode. Previously stored patch objects literally as jsCode, producing [object Object]. Now reads current value, applies find/replace sequentially, writes back the string. Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
237 lines
8.3 KiB
JavaScript
237 lines
8.3 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.sanitizeNode = sanitizeNode;
|
|
exports.sanitizeWorkflowNodes = sanitizeWorkflowNodes;
|
|
exports.validateNodeMetadata = validateNodeMetadata;
|
|
const logger_1 = require("../utils/logger");
|
|
const OPERATOR_CORRECTIONS = {
|
|
'isEmpty': 'empty',
|
|
'isNotEmpty': 'notEmpty',
|
|
};
|
|
const UNARY_OPERATORS = new Set([
|
|
'true',
|
|
'false',
|
|
'isNumeric',
|
|
'empty',
|
|
'notEmpty',
|
|
'exists',
|
|
'notExists',
|
|
]);
|
|
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(sanitizeNode)
|
|
};
|
|
}
|
|
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(sanitizeCondition);
|
|
}
|
|
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`);
|
|
sanitized.type = inferDataType(typeValue);
|
|
sanitized.operation = typeValue;
|
|
}
|
|
}
|
|
if (sanitized.operation && OPERATOR_CORRECTIONS[sanitized.operation]) {
|
|
sanitized.operation = OPERATOR_CORRECTIONS[sanitized.operation];
|
|
}
|
|
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'];
|
|
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';
|
|
}
|
|
const objectOps = ['empty', 'notEmpty', 'exists', 'notExists'];
|
|
if (objectOps.includes(operation)) {
|
|
return 'object';
|
|
}
|
|
return 'string';
|
|
}
|
|
function isUnaryOperator(operation) {
|
|
return UNARY_OPERATORS.has(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
|