refactor(core): Remove legacy expression evaluator (#14518)

This commit is contained in:
Iván Ovejero
2025-05-13 17:29:48 +02:00
committed by GitHub
parent 694af6c9f0
commit 131baabb7f
22 changed files with 221 additions and 423 deletions

View File

@@ -1,4 +1,3 @@
import * as tmpl from '@n8n_io/riot-tmpl';
import { DateTime, Duration, Interval } from 'luxon';
import { ApplicationError } from './errors/application.error';
@@ -53,7 +52,7 @@ export class Expression {
constructor(private readonly workflow: Workflow) {}
static resolveWithoutWorkflow(expression: string, data: IDataObject = {}) {
return tmpl.tmpl(expression, data);
return evaluateExpression(expression, data);
}
/**
@@ -327,10 +326,7 @@ export class Expression {
return returnValue;
}
private renderExpression(
expression: string,
data: IWorkflowDataProxyData,
): tmpl.ReturnValue | undefined {
private renderExpression(expression: string, data: IWorkflowDataProxyData) {
try {
return evaluateExpression(expression, data);
} catch (error) {

View File

@@ -1,155 +1,21 @@
import type { ReturnValue, TmplDifference } from '@n8n/tournament';
import { Tournament } from '@n8n/tournament';
import * as tmpl from '@n8n_io/riot-tmpl';
import { PrototypeSanitizer } from './ExpressionSandboxing';
import type { ExpressionEvaluatorType } from './Interfaces';
import * as LoggerProxy from './LoggerProxy';
type Evaluator = (expr: string, data: unknown) => tmpl.ReturnValue;
type Evaluator = (expr: string, data: unknown) => string | null | (() => unknown);
type ErrorHandler = (error: Error) => void;
type DifferenceHandler = (expr: string) => void;
// Set it to use double curly brackets instead of single ones
tmpl.brackets.set('{{ }}');
let errorHandler: ErrorHandler = () => {};
let differenceHandler: DifferenceHandler = () => {};
const differenceChecker = (diff: TmplDifference) => {
try {
if (diff.same) {
return;
}
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
if (diff.has?.function || diff.has?.templateString) {
return;
}
if (diff.expression === 'UNPARSEABLE') {
differenceHandler(diff.expression);
} else {
differenceHandler(diff.expression.value);
}
} catch {
LoggerProxy.error('Expression evaluator difference checker failed');
}
};
const errorHandler: ErrorHandler = () => {};
const tournamentEvaluator = new Tournament(errorHandler, undefined, undefined, {
before: [],
after: [PrototypeSanitizer],
});
let evaluator: Evaluator = tmpl.tmpl;
let currentEvaluatorType: ExpressionEvaluatorType = 'tmpl';
let diffExpressions = false;
const evaluator: Evaluator = tournamentEvaluator.execute.bind(tournamentEvaluator);
export const setErrorHandler = (handler: ErrorHandler) => {
errorHandler = handler;
tmpl.tmpl.errorHandler = handler;
tournamentEvaluator.errorHandler = handler;
};
export const setEvaluator = (evalType: ExpressionEvaluatorType) => {
currentEvaluatorType = evalType;
if (evalType === 'tmpl') {
evaluator = tmpl.tmpl;
} else if (evalType === 'tournament') {
evaluator = tournamentEvaluator.execute.bind(tournamentEvaluator);
}
};
export const setDiffReporter = (reporter: (expr: string) => void) => {
differenceHandler = reporter;
};
export const setDifferEnabled = (enabled: boolean) => {
diffExpressions = enabled;
};
const diffCache: Record<string, TmplDifference | null> = {};
export const checkEvaluatorDifferences = (expr: string): TmplDifference | null => {
if (expr in diffCache) {
return diffCache[expr];
}
let diff: TmplDifference | null;
try {
diff = tournamentEvaluator.tmplDiff(expr);
} catch {
// We don't include the expression for privacy reasons
try {
differenceHandler('ERROR');
} catch {}
diff = null;
}
if (diff?.same === false) {
differenceChecker(diff);
}
diffCache[expr] = diff;
return diff;
};
export const getEvaluator = () => {
return evaluator;
};
export const evaluateExpression: Evaluator = (expr, data) => {
if (!diffExpressions) {
return evaluator(expr, data);
}
const diff = checkEvaluatorDifferences(expr);
// We already know that they're different so don't bother
// evaluating with both evaluators
if (!diff?.same) {
return evaluator(expr, data);
}
let tmplValue: tmpl.ReturnValue;
let tournValue: ReturnValue;
let wasTmplError = false;
let tmplError: unknown;
let wasTournError = false;
let tournError: unknown;
try {
tmplValue = tmpl.tmpl(expr, data);
} catch (error) {
tmplError = error;
wasTmplError = true;
}
try {
tournValue = tournamentEvaluator.execute(expr, data);
} catch (error) {
tournError = error;
wasTournError = true;
}
if (
wasTmplError !== wasTournError ||
JSON.stringify(tmplValue!) !== JSON.stringify(tournValue!)
) {
try {
if (diff.expression) {
differenceHandler(diff.expression.value);
} else {
differenceHandler('VALUEDIFF');
}
} catch {
LoggerProxy.error('Failed to report error difference');
}
}
if (currentEvaluatorType === 'tmpl') {
if (wasTmplError) {
throw tmplError;
}
return tmplValue!;
}
if (wasTournError) {
throw tournError;
}
return tournValue!;
return evaluator(expr, data);
};

View File

@@ -134,7 +134,7 @@ export const extendTransform = (expression: string): { code: string } | undefine
const chainNumber = currentChain;
currentChain += 1;
// This is to match our fork of tmpl
// This is to match behavior in our original expression evaluator (tmpl)
const globalIdentifier = types.builders.identifier(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
typeof window !== 'object' ? 'global' : 'window',

View File

@@ -6,8 +6,9 @@ export interface ExpressionText {
export interface ExpressionCode {
type: 'code';
text: string;
// tmpl has different behaviours if the last expression
// doesn't close itself.
// This is to match behavior in our original expression evaluator (tmpl),
// which has different behaviours if the last expression doesn't close itself.
hasClosingBrackets: boolean;
}

View File

@@ -2872,8 +2872,6 @@ export interface ICheckProcessedContextData {
};
}
export type ExpressionEvaluatorType = 'tmpl' | 'tournament';
export type N8nAIProviderType = 'openai' | 'unknown';
export interface SecretsHelpersBase {

View File

@@ -1,5 +1,4 @@
import * as LoggerProxy from './LoggerProxy';
export * as ExpressionEvaluatorProxy from './ExpressionEvaluatorProxy';
import * as NodeHelpers from './NodeHelpers';
import * as ObservableObject from './ObservableObject';
import * as TelemetryHelpers from './TelemetryHelpers';