fix: correct operator names, connection types, and implement __patch_find_replace (#665, #659, #642) (#672)

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>
This commit is contained in:
Romuald Członkowski
2026-03-27 16:26:21 +01:00
committed by GitHub
parent de2abaf89d
commit 6be9ffa53e
28 changed files with 522 additions and 96 deletions

View File

@@ -13,6 +13,23 @@ import { INodeParameters } from 'n8n-workflow';
import { logger } from '../utils/logger';
import { WorkflowNode } from '../types/n8n-api';
/** Legacy operator names that n8n no longer recognizes, mapped to their correct names. */
const OPERATOR_CORRECTIONS: Record<string, string> = {
'isEmpty': 'empty',
'isNotEmpty': 'notEmpty',
};
/** Operators that take no right-hand value and require singleValue: true. */
const UNARY_OPERATORS = new Set([
'true',
'false',
'isNumeric',
'empty',
'notEmpty',
'exists',
'notExists',
]);
/**
* Sanitize a single node by adding required metadata
*/
@@ -162,29 +179,28 @@ function sanitizeOperator(operator: any): any {
const sanitized = { ...operator };
// Fix common mistake: type field used for operation name
// WRONG: {type: "isNotEmpty"}
// RIGHT: {type: "string", operation: "isNotEmpty"}
// WRONG: {type: "notEmpty"}
// RIGHT: {type: "string", operation: "notEmpty"}
if (sanitized.type && !sanitized.operation) {
// Check if type value looks like an operation (lowercase, no dots)
const typeValue = sanitized.type as string;
if (isOperationName(typeValue)) {
logger.debug(`Fixing operator structure: converting type="${typeValue}" to operation`);
// Infer data type from operation
const dataType = inferDataType(typeValue);
sanitized.type = dataType;
sanitized.type = inferDataType(typeValue);
sanitized.operation = typeValue;
}
}
// Auto-correct legacy operator names to n8n-recognized names
if (sanitized.operation && OPERATOR_CORRECTIONS[sanitized.operation]) {
sanitized.operation = OPERATOR_CORRECTIONS[sanitized.operation];
}
// Set singleValue based on operator type
if (sanitized.operation) {
if (isUnaryOperator(sanitized.operation)) {
// Unary operators require singleValue: true
sanitized.singleValue = true;
} else {
// Binary operators should NOT have singleValue (or it should be false/undefined)
// Remove it to prevent UI errors
// Binary operators should NOT have singleValue — remove it to prevent UI errors
delete sanitized.singleValue;
}
}
@@ -207,7 +223,7 @@ function isOperationName(value: string): boolean {
*/
function inferDataType(operation: string): string {
// Boolean operations
const booleanOps = ['true', 'false', 'isEmpty', 'isNotEmpty'];
const booleanOps = ['true', 'false'];
if (booleanOps.includes(operation)) {
return 'boolean';
}
@@ -225,7 +241,6 @@ function inferDataType(operation: string): string {
}
// Object operations: empty/notEmpty/exists/notExists are generic object-level checks
// (distinct from isEmpty/isNotEmpty which are boolean-typed operations)
const objectOps = ['empty', 'notEmpty', 'exists', 'notExists'];
if (objectOps.includes(operation)) {
return 'object';
@@ -239,18 +254,7 @@ function inferDataType(operation: string): string {
* Check if operator is unary (requires singleValue: true)
*/
function isUnaryOperator(operation: string): boolean {
const unaryOps = [
'isEmpty',
'isNotEmpty',
'true',
'false',
'isNumeric',
'empty',
'notEmpty',
'exists',
'notExists'
];
return unaryOps.includes(operation);
return UNARY_OPERATORS.has(operation);
}
/**