diff --git a/packages/cli/src/ExpressionEvalator.ts b/packages/cli/src/ExpressionEvalator.ts new file mode 100644 index 0000000000000..1b53f845dc1c5 --- /dev/null +++ b/packages/cli/src/ExpressionEvalator.ts @@ -0,0 +1,14 @@ +import config from '@/config'; +import { ErrorReporterProxy, ExpressionEvaluatorProxy } from 'n8n-workflow'; + +export const initExpressionEvaluator = () => { + ExpressionEvaluatorProxy.setEvaluator(config.getEnv('expression.evaluator')); + ExpressionEvaluatorProxy.setDifferEnabled(config.getEnv('expression.reportDifference')); + ExpressionEvaluatorProxy.setDiffReporter((expr) => { + ErrorReporterProxy.warn('Expression difference', { + extra: { + expression: expr, + }, + }); + }); +}; diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 842cf797662e9..b17b2d8bb9b9f 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -336,6 +336,9 @@ export class Server extends AbstractServer { variables: { limit: 0, }, + expressions: { + evaluator: config.getEnv('expression.evaluator'), + }, banners: { dismissed: [], }, diff --git a/packages/cli/src/commands/BaseCommand.ts b/packages/cli/src/commands/BaseCommand.ts index 6aa1e12176999..039660843a674 100644 --- a/packages/cli/src/commands/BaseCommand.ts +++ b/packages/cli/src/commands/BaseCommand.ts @@ -21,6 +21,7 @@ import { InternalHooks } from '@/InternalHooks'; import { PostHogClient } from '@/posthog'; import { License } from '@/License'; import { ExternalSecretsManager } from '@/ExternalSecrets/ExternalSecretsManager.ee'; +import { initExpressionEvaluator } from '@/ExpressionEvalator'; export abstract class BaseCommand extends Command { protected logger = LoggerProxy.init(getLogger()); @@ -39,6 +40,7 @@ export abstract class BaseCommand extends Command { async init(): Promise { await initErrorHandling(); + initExpressionEvaluator(); process.once('SIGTERM', async () => this.stopProcess()); process.once('SIGINT', async () => this.stopProcess()); diff --git a/packages/cli/src/config/schema.ts b/packages/cli/src/config/schema.ts index 1704be42ba8b2..d035de34ae2f7 100644 --- a/packages/cli/src/config/schema.ts +++ b/packages/cli/src/config/schema.ts @@ -1199,6 +1199,21 @@ export const schema = { }, }, + expression: { + evaluator: { + doc: 'Expression evaluator to use', + format: ['tmpl', 'tournament'] as const, + default: 'tournament', + env: 'N8N_EXPRESSION_EVALUATOR', + }, + reportDifference: { + doc: 'Whether to report differences in the evaluator outputs', + format: Boolean, + default: false, + env: 'N8N_EXPRESSION_REPORT_DIFFERENCE', + }, + }, + sourceControl: { defaultKeyPairType: { doc: 'Default SSH key type to use when generating SSH keys', diff --git a/packages/editor-ui/src/App.vue b/packages/editor-ui/src/App.vue index a03cf59c99794..aab8297a9a427 100644 --- a/packages/editor-ui/src/App.vue +++ b/packages/editor-ui/src/App.vue @@ -59,6 +59,7 @@ import { useHistoryHelper } from '@/composables/useHistoryHelper'; import { newVersions } from '@/mixins/newVersions'; import { useRoute } from 'vue-router'; import { useExternalHooks } from '@/composables'; +import { ExpressionEvaluatorProxy } from 'n8n-workflow'; export default defineComponent({ name: 'App', @@ -148,6 +149,7 @@ export default defineComponent({ }, async initialize(): Promise { await this.initSettings(); + ExpressionEvaluatorProxy.setEvaluator(useSettingsStore().settings.expressions.evaluator); await Promise.all([this.loginWithCookie(), this.initTemplates()]); }, trackPage(): void { diff --git a/packages/workflow/package.json b/packages/workflow/package.json index f342897ec459d..143a5f9a7ca1f 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -48,6 +48,7 @@ "@types/xml2js": "^0.4.11" }, "dependencies": { + "@n8n/tournament": "^1.0.2", "@n8n_io/riot-tmpl": "^4.0.0", "ast-types": "0.15.2", "crypto-js": "^4.1.1", diff --git a/packages/workflow/src/Expression.ts b/packages/workflow/src/Expression.ts index 6d1ab313c94d2..7b5a6633a1bc1 100644 --- a/packages/workflow/src/Expression.ts +++ b/packages/workflow/src/Expression.ts @@ -1,5 +1,5 @@ -import * as tmpl from '@n8n_io/riot-tmpl'; import { DateTime, Duration, Interval } from 'luxon'; +import * as tmpl from '@n8n_io/riot-tmpl'; import type { IDataObject, @@ -22,6 +22,7 @@ import type { Workflow } from './Workflow'; import { extend, extendOptional } from './Extensions'; import { extendedFunctions } from './Extensions/ExtendedFunctions'; import { extendSyntax } from './Extensions/ExpressionExtension'; +import { evaluateExpression, setErrorHandler } from './ExpressionEvaluatorProxy'; const IS_FRONTEND_IN_DEV_MODE = typeof process === 'object' && @@ -40,13 +41,10 @@ export const isExpressionError = (error: unknown): error is ExpressionError => export const isTypeError = (error: unknown): error is TypeError => error instanceof TypeError || (error instanceof Error && error.name === 'TypeError'); -// Set it to use double curly brackets instead of single ones -tmpl.brackets.set('{{ }}'); - // Make sure that error get forwarded -tmpl.tmpl.errorHandler = (error: Error) => { +setErrorHandler((error: Error) => { if (isExpressionError(error)) throw error; -}; +}); // eslint-disable-next-line @typescript-eslint/naming-convention const AsyncFunction = (async () => {}).constructor as FunctionConstructor; @@ -339,7 +337,7 @@ export class Expression { [Function, AsyncFunction].forEach(({ prototype }) => Object.defineProperty(prototype, 'constructor', { value: fnConstructors.mock }), ); - return tmpl.tmpl(expression, data); + return evaluateExpression(expression, data); } catch (error) { if (isExpressionError(error)) throw error; diff --git a/packages/workflow/src/ExpressionEvaluatorProxy.ts b/packages/workflow/src/ExpressionEvaluatorProxy.ts new file mode 100644 index 0000000000000..60e33e5a7d15c --- /dev/null +++ b/packages/workflow/src/ExpressionEvaluatorProxy.ts @@ -0,0 +1,149 @@ +import * as tmpl from '@n8n_io/riot-tmpl'; +import type { ReturnValue, TmplDifference } from '@n8n/tournament'; +import { Tournament } from '@n8n/tournament'; +import type { ExpressionEvaluatorType } from './Interfaces'; +import * as LoggerProxy from './LoggerProxy'; + +type Evaluator = (expr: string, data: unknown) => tmpl.ReturnValue; +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; + } + 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 tournamentEvaluator = new Tournament(errorHandler, undefined); +let evaluator: Evaluator = tmpl.tmpl; +let currentEvaluatorType: ExpressionEvaluatorType = 'tmpl'; +let diffExpressions = false; + +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 = {}; + +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!; +}; diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index bda4f19f389a6..d29c51054d620 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -2117,6 +2117,8 @@ export interface IPublicApiSettings { export type ILogLevel = 'info' | 'debug' | 'warn' | 'error' | 'verbose' | 'silent'; +export type ExpressionEvaluatorType = 'tmpl' | 'tournament'; + export interface IN8nUISettings { endpointWebhook: string; endpointWebhookTest: string; @@ -2203,6 +2205,9 @@ export interface IN8nUISettings { variables: { limit: number; }; + expressions: { + evaluator: ExpressionEvaluatorType; + }; mfa: { enabled: boolean; }; diff --git a/packages/workflow/src/index.ts b/packages/workflow/src/index.ts index ff6b1d7316f1c..ddc41baca44de 100644 --- a/packages/workflow/src/index.ts +++ b/packages/workflow/src/index.ts @@ -1,5 +1,6 @@ import * as LoggerProxy from './LoggerProxy'; export * as ErrorReporterProxy from './ErrorReporterProxy'; +export * as ExpressionEvaluatorProxy from './ExpressionEvaluatorProxy'; import * as NodeHelpers from './NodeHelpers'; import * as ObservableObject from './ObservableObject'; import * as TelemetryHelpers from './TelemetryHelpers'; diff --git a/packages/workflow/test/Expression.test.ts b/packages/workflow/test/Expression.test.ts index bcfc44f613215..c38032b0010bd 100644 --- a/packages/workflow/test/Expression.test.ts +++ b/packages/workflow/test/Expression.test.ts @@ -11,221 +11,227 @@ import { baseFixtures } from './ExpressionFixtures/base'; import type { INodeExecutionData } from '@/Interfaces'; import { extendSyntax } from '@/Extensions/ExpressionExtension'; import { ExpressionError } from '@/ExpressionError'; +import { setDifferEnabled, setEvaluator } from '@/ExpressionEvaluatorProxy'; + +setDifferEnabled(true); + +for (const evaluator of ['tmpl', 'tournament'] as const) { + setEvaluator(evaluator); + describe(`Expression (with ${evaluator})`, () => { + describe('getParameterValue()', () => { + const nodeTypes = Helpers.NodeTypes(); + const workflow = new Workflow({ + nodes: [ + { + name: 'node', + typeVersion: 1, + type: 'test.set', + id: 'uuid-1234', + position: [0, 0], + parameters: {}, + }, + ], + connections: {}, + active: false, + nodeTypes, + }); + const expression = new Expression(workflow); -describe('Expression', () => { - describe('getParameterValue()', () => { - const nodeTypes = Helpers.NodeTypes(); - const workflow = new Workflow({ - nodes: [ - { - name: 'node', - typeVersion: 1, - type: 'test.set', - id: 'uuid-1234', - position: [0, 0], - parameters: {}, - }, - ], - connections: {}, - active: false, - nodeTypes, - }); - const expression = new Expression(workflow); + const evaluate = (value: string) => + expression.getParameterValue(value, null, 0, 0, 'node', [], 'manual', '', {}); - const evaluate = (value: string) => - expression.getParameterValue(value, null, 0, 0, 'node', [], 'manual', '', {}); + it('should not be able to use global built-ins from denylist', () => { + expect(evaluate('={{document}}')).toEqual({}); + expect(evaluate('={{window}}')).toEqual({}); - it('should not be able to use global built-ins from denylist', () => { - expect(evaluate('={{document}}')).toEqual({}); - expect(evaluate('={{window}}')).toEqual({}); + expect(evaluate('={{Window}}')).toEqual({}); + expect(evaluate('={{globalThis}}')).toEqual({}); + expect(evaluate('={{self}}')).toEqual({}); - expect(evaluate('={{Window}}')).toEqual({}); - expect(evaluate('={{globalThis}}')).toEqual({}); - expect(evaluate('={{self}}')).toEqual({}); + expect(evaluate('={{alert}}')).toEqual({}); + expect(evaluate('={{prompt}}')).toEqual({}); + expect(evaluate('={{confirm}}')).toEqual({}); - expect(evaluate('={{alert}}')).toEqual({}); - expect(evaluate('={{prompt}}')).toEqual({}); - expect(evaluate('={{confirm}}')).toEqual({}); + expect(evaluate('={{eval}}')).toEqual({}); + expect(evaluate('={{uneval}}')).toEqual({}); + expect(evaluate('={{setTimeout}}')).toEqual({}); + expect(evaluate('={{setInterval}}')).toEqual({}); + expect(evaluate('={{Function}}')).toEqual({}); - expect(evaluate('={{eval}}')).toEqual({}); - expect(evaluate('={{uneval}}')).toEqual({}); - expect(evaluate('={{setTimeout}}')).toEqual({}); - expect(evaluate('={{setInterval}}')).toEqual({}); - expect(evaluate('={{Function}}')).toEqual({}); + expect(evaluate('={{fetch}}')).toEqual({}); + expect(evaluate('={{XMLHttpRequest}}')).toEqual({}); - expect(evaluate('={{fetch}}')).toEqual({}); - expect(evaluate('={{XMLHttpRequest}}')).toEqual({}); + expect(evaluate('={{Promise}}')).toEqual({}); + expect(evaluate('={{Generator}}')).toEqual({}); + expect(evaluate('={{GeneratorFunction}}')).toEqual({}); + expect(evaluate('={{AsyncFunction}}')).toEqual({}); + expect(evaluate('={{AsyncGenerator}}')).toEqual({}); + expect(evaluate('={{AsyncGeneratorFunction}}')).toEqual({}); - expect(evaluate('={{Promise}}')).toEqual({}); - expect(evaluate('={{Generator}}')).toEqual({}); - expect(evaluate('={{GeneratorFunction}}')).toEqual({}); - expect(evaluate('={{AsyncFunction}}')).toEqual({}); - expect(evaluate('={{AsyncGenerator}}')).toEqual({}); - expect(evaluate('={{AsyncGeneratorFunction}}')).toEqual({}); + expect(evaluate('={{WebAssembly}}')).toEqual({}); - expect(evaluate('={{WebAssembly}}')).toEqual({}); + expect(evaluate('={{Reflect}}')).toEqual({}); + expect(evaluate('={{Proxy}}')).toEqual({}); - expect(evaluate('={{Reflect}}')).toEqual({}); - expect(evaluate('={{Proxy}}')).toEqual({}); + expect(evaluate('={{constructor}}')).toEqual({}); - expect(evaluate('={{constructor}}')).toEqual({}); + expect(evaluate('={{escape}}')).toEqual({}); + expect(evaluate('={{unescape}}')).toEqual({}); + }); - expect(evaluate('={{escape}}')).toEqual({}); - expect(evaluate('={{unescape}}')).toEqual({}); - }); + it('should be able to use global built-ins from allowlist', () => { + expect(evaluate('={{new Date()}}')).toBeInstanceOf(Date); + expect(evaluate('={{DateTime.now().toLocaleString()}}')).toEqual( + DateTime.now().toLocaleString(), + ); + expect(evaluate('={{Interval.after(new Date(), 100)}}')).toEqual( + Interval.after(new Date(), 100), + ); + expect(evaluate('={{Duration.fromMillis(100)}}')).toEqual(Duration.fromMillis(100)); + + expect(evaluate('={{new Object()}}')).toEqual(new Object()); + + expect(evaluate('={{new Array()}}')).toEqual([]); + expect(evaluate('={{new Int8Array()}}')).toEqual(new Int8Array()); + expect(evaluate('={{new Uint8Array()}}')).toEqual(new Uint8Array()); + expect(evaluate('={{new Uint8ClampedArray()}}')).toEqual(new Uint8ClampedArray()); + expect(evaluate('={{new Int16Array()}}')).toEqual(new Int16Array()); + expect(evaluate('={{new Uint16Array()}}')).toEqual(new Uint16Array()); + expect(evaluate('={{new Int32Array()}}')).toEqual(new Int32Array()); + expect(evaluate('={{new Uint32Array()}}')).toEqual(new Uint32Array()); + expect(evaluate('={{new Float32Array()}}')).toEqual(new Float32Array()); + expect(evaluate('={{new Float64Array()}}')).toEqual(new Float64Array()); + expect(evaluate('={{new BigInt64Array()}}')).toEqual(new BigInt64Array()); + expect(evaluate('={{new BigUint64Array()}}')).toEqual(new BigUint64Array()); + + expect(evaluate('={{new Map()}}')).toEqual(new Map()); + expect(evaluate('={{new WeakMap()}}')).toEqual(new WeakMap()); + expect(evaluate('={{new Set()}}')).toEqual(new Set()); + expect(evaluate('={{new WeakSet()}}')).toEqual(new WeakSet()); + + expect(evaluate('={{new Error()}}')).toEqual(new Error()); + expect(evaluate('={{new TypeError()}}')).toEqual(new TypeError()); + expect(evaluate('={{new SyntaxError()}}')).toEqual(new SyntaxError()); + expect(evaluate('={{new EvalError()}}')).toEqual(new EvalError()); + expect(evaluate('={{new RangeError()}}')).toEqual(new RangeError()); + expect(evaluate('={{new ReferenceError()}}')).toEqual(new ReferenceError()); + expect(evaluate('={{new URIError()}}')).toEqual(new URIError()); + + expect(evaluate('={{Intl}}')).toEqual(Intl); + + expect(evaluate('={{new String()}}')).toEqual(new String()); + expect(evaluate("={{new RegExp('')}}")).toEqual(new RegExp('')); + + expect(evaluate('={{Math}}')).toEqual(Math); + expect(evaluate('={{new Number()}}')).toEqual(new Number()); + expect(evaluate("={{BigInt('1')}}")).toEqual(BigInt('1')); + expect(evaluate('={{Infinity}}')).toEqual(Infinity); + expect(evaluate('={{NaN}}')).toEqual(NaN); + expect(evaluate('={{isFinite(1)}}')).toEqual(isFinite(1)); + expect(evaluate('={{isNaN(1)}}')).toEqual(isNaN(1)); + expect(evaluate("={{parseFloat('1')}}")).toEqual(parseFloat('1')); + expect(evaluate("={{parseInt('1', 10)}}")).toEqual(parseInt('1', 10)); + + expect(evaluate('={{JSON.stringify({})}}')).toEqual(JSON.stringify({})); + expect(evaluate('={{new ArrayBuffer(10)}}')).toEqual(new ArrayBuffer(10)); + expect(evaluate('={{new SharedArrayBuffer(10)}}')).toEqual(new SharedArrayBuffer(10)); + expect(evaluate('={{Atomics}}')).toEqual(Atomics); + expect(evaluate('={{new DataView(new ArrayBuffer(1))}}')).toEqual( + new DataView(new ArrayBuffer(1)), + ); + + expect(evaluate("={{encodeURI('https://google.com')}}")).toEqual( + encodeURI('https://google.com'), + ); + expect(evaluate("={{encodeURIComponent('https://google.com')}}")).toEqual( + encodeURIComponent('https://google.com'), + ); + expect(evaluate("={{decodeURI('https://google.com')}}")).toEqual( + decodeURI('https://google.com'), + ); + expect(evaluate("={{decodeURIComponent('https://google.com')}}")).toEqual( + decodeURIComponent('https://google.com'), + ); + + expect(evaluate('={{Boolean(1)}}')).toEqual(Boolean(1)); + expect(evaluate('={{Symbol(1).toString()}}')).toEqual(Symbol(1).toString()); + }); - it('should be able to use global built-ins from allowlist', () => { - expect(evaluate('={{new Date()}}')).toBeInstanceOf(Date); - expect(evaluate('={{DateTime.now().toLocaleString()}}')).toEqual( - DateTime.now().toLocaleString(), - ); - expect(evaluate('={{Interval.after(new Date(), 100)}}')).toEqual( - Interval.after(new Date(), 100), - ); - expect(evaluate('={{Duration.fromMillis(100)}}')).toEqual(Duration.fromMillis(100)); - - expect(evaluate('={{new Object()}}')).toEqual(new Object()); - - expect(evaluate('={{new Array()}}')).toEqual([]); - expect(evaluate('={{new Int8Array()}}')).toEqual(new Int8Array()); - expect(evaluate('={{new Uint8Array()}}')).toEqual(new Uint8Array()); - expect(evaluate('={{new Uint8ClampedArray()}}')).toEqual(new Uint8ClampedArray()); - expect(evaluate('={{new Int16Array()}}')).toEqual(new Int16Array()); - expect(evaluate('={{new Uint16Array()}}')).toEqual(new Uint16Array()); - expect(evaluate('={{new Int32Array()}}')).toEqual(new Int32Array()); - expect(evaluate('={{new Uint32Array()}}')).toEqual(new Uint32Array()); - expect(evaluate('={{new Float32Array()}}')).toEqual(new Float32Array()); - expect(evaluate('={{new Float64Array()}}')).toEqual(new Float64Array()); - expect(evaluate('={{new BigInt64Array()}}')).toEqual(new BigInt64Array()); - expect(evaluate('={{new BigUint64Array()}}')).toEqual(new BigUint64Array()); - - expect(evaluate('={{new Map()}}')).toEqual(new Map()); - expect(evaluate('={{new WeakMap()}}')).toEqual(new WeakMap()); - expect(evaluate('={{new Set()}}')).toEqual(new Set()); - expect(evaluate('={{new WeakSet()}}')).toEqual(new WeakSet()); - - expect(evaluate('={{new Error()}}')).toEqual(new Error()); - expect(evaluate('={{new TypeError()}}')).toEqual(new TypeError()); - expect(evaluate('={{new SyntaxError()}}')).toEqual(new SyntaxError()); - expect(evaluate('={{new EvalError()}}')).toEqual(new EvalError()); - expect(evaluate('={{new RangeError()}}')).toEqual(new RangeError()); - expect(evaluate('={{new ReferenceError()}}')).toEqual(new ReferenceError()); - expect(evaluate('={{new URIError()}}')).toEqual(new URIError()); - - expect(evaluate('={{Intl}}')).toEqual(Intl); - - expect(evaluate('={{new String()}}')).toEqual(new String()); - expect(evaluate("={{new RegExp('')}}")).toEqual(new RegExp('')); - - expect(evaluate('={{Math}}')).toEqual(Math); - expect(evaluate('={{new Number()}}')).toEqual(new Number()); - expect(evaluate("={{BigInt('1')}}")).toEqual(BigInt('1')); - expect(evaluate('={{Infinity}}')).toEqual(Infinity); - expect(evaluate('={{NaN}}')).toEqual(NaN); - expect(evaluate('={{isFinite(1)}}')).toEqual(isFinite(1)); - expect(evaluate('={{isNaN(1)}}')).toEqual(isNaN(1)); - expect(evaluate("={{parseFloat('1')}}")).toEqual(parseFloat('1')); - expect(evaluate("={{parseInt('1', 10)}}")).toEqual(parseInt('1', 10)); - - expect(evaluate('={{JSON.stringify({})}}')).toEqual(JSON.stringify({})); - expect(evaluate('={{new ArrayBuffer(10)}}')).toEqual(new ArrayBuffer(10)); - expect(evaluate('={{new SharedArrayBuffer(10)}}')).toEqual(new SharedArrayBuffer(10)); - expect(evaluate('={{Atomics}}')).toEqual(Atomics); - expect(evaluate('={{new DataView(new ArrayBuffer(1))}}')).toEqual( - new DataView(new ArrayBuffer(1)), - ); - - expect(evaluate("={{encodeURI('https://google.com')}}")).toEqual( - encodeURI('https://google.com'), - ); - expect(evaluate("={{encodeURIComponent('https://google.com')}}")).toEqual( - encodeURIComponent('https://google.com'), - ); - expect(evaluate("={{decodeURI('https://google.com')}}")).toEqual( - decodeURI('https://google.com'), - ); - expect(evaluate("={{decodeURIComponent('https://google.com')}}")).toEqual( - decodeURIComponent('https://google.com'), - ); - - expect(evaluate('={{Boolean(1)}}')).toEqual(Boolean(1)); - expect(evaluate('={{Symbol(1).toString()}}')).toEqual(Symbol(1).toString()); + it('should not able to do arbitrary code execution', () => { + const testFn = jest.fn(); + Object.assign(global, { testFn }); + expect(() => evaluate("={{ Date['constructor']('testFn()')()}}")).toThrowError( + new ExpressionError('Arbitrary code execution detected'), + ); + expect(testFn).not.toHaveBeenCalled(); + }); }); - it('should not able to do arbitrary code execution', () => { - const testFn = jest.fn(); - Object.assign(global, { testFn }); - expect(() => evaluate("={{ Date['constructor']('testFn()')()}}")).toThrowError( - new ExpressionError('Arbitrary code execution detected'), - ); - expect(testFn).not.toHaveBeenCalled(); - }); - }); + describe('Test all expression value fixtures', () => { + const nodeTypes = Helpers.NodeTypes(); + const workflow = new Workflow({ + nodes: [ + { + name: 'node', + typeVersion: 1, + type: 'test.set', + id: 'uuid-1234', + position: [0, 0], + parameters: {}, + }, + ], + connections: {}, + active: false, + nodeTypes, + }); - describe('Test all expression value fixtures', () => { - const nodeTypes = Helpers.NodeTypes(); - const workflow = new Workflow({ - nodes: [ - { - name: 'node', - typeVersion: 1, - type: 'test.set', - id: 'uuid-1234', - position: [0, 0], - parameters: {}, - }, - ], - connections: {}, - active: false, - nodeTypes, + const expression = new Expression(workflow); + + const evaluate = (value: string, data: INodeExecutionData[]) => { + const itemIndex = data.length === 0 ? -1 : 0; + return expression.getParameterValue( + value, + null, + 0, + itemIndex, + 'node', + data, + 'manual', + '', + {}, + ); + }; + + for (const t of baseFixtures) { + if (!t.tests.some((test) => test.type === 'evaluation')) { + continue; + } + test(t.expression, () => { + for (const test of t.tests.filter( + (test) => test.type === 'evaluation', + ) as ExpressionTestEvaluation[]) { + expect( + evaluate(t.expression, test.input.map((d) => ({ json: d })) as any), + ).toStrictEqual(test.output); + } + }); + } }); - const expression = new Expression(workflow); - - const evaluate = (value: string, data: INodeExecutionData[]) => { - const itemIndex = data.length === 0 ? -1 : 0; - return expression.getParameterValue( - value, - null, - 0, - itemIndex, - 'node', - data, - 'manual', - '', - {}, - ); - }; - - for (const t of baseFixtures) { - if (!t.tests.some((test) => test.type === 'evaluation')) { - continue; - } - test(t.expression, () => { - for (const test of t.tests.filter( - (test) => test.type === 'evaluation', - ) as ExpressionTestEvaluation[]) { - expect(evaluate(t.expression, test.input.map((d) => ({ json: d })) as any)).toStrictEqual( - test.output, - ); + describe('Test all expression transform fixtures', () => { + for (const t of baseFixtures) { + if (!t.tests.some((test) => test.type === 'transform')) { + continue; } - }); - } - }); - - describe('Test all expression transform fixtures', () => { - for (const t of baseFixtures) { - if (!t.tests.some((test) => test.type === 'transform')) { - continue; + test(t.expression, () => { + for (const test of t.tests.filter( + (test) => test.type === 'transform', + ) as ExpressionTestTransform[]) { + const expr = t.expression; + expect(extendSyntax(expr, test.forceTransform)).toEqual(test.result ?? expr); + } + }); } - test(t.expression, () => { - for (const test of t.tests.filter( - (test) => test.type === 'transform', - ) as ExpressionTestTransform[]) { - const expr = t.expression; - expect(extendSyntax(expr, test.forceTransform)).toEqual(test.result ?? expr); - } - }); - } + }); }); -}); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d966251bb2bd7..7ae16a82e49a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1277,6 +1277,9 @@ importers: packages/workflow: dependencies: + '@n8n/tournament': + specifier: ^1.0.2 + version: 1.0.2 '@n8n_io/riot-tmpl': specifier: ^4.0.0 version: 4.0.0 @@ -4635,6 +4638,16 @@ packages: - '@lezer/common' dev: false + /@n8n/tournament@1.0.2: + resolution: {integrity: sha512-fTpi7F8ra5flGSVfRzohPyG7czAAKCZPlLjdKdwbLJivLoI/Ekhgodov1jfVSCVFVbwQ06gRQRxLEDzl2jl8ig==} + engines: {node: '>=18.10', pnpm: '>=8.6'} + dependencies: + '@n8n_io/riot-tmpl': 4.0.1 + ast-types: 0.16.1 + esprima-next: 5.8.4 + recast: 0.22.0 + dev: false + /@n8n/vm2@3.9.20: resolution: {integrity: sha512-qk2oJYkuFRVSTxoro4obX/sv/wT1pViZjHh/isjOvFB93D52QIg3TCjMPsHOfHTmkxCKJffjLrUvjIwvWzSMCQ==} engines: {node: '>=18.10', pnpm: '>=8.6.12'} @@ -4660,6 +4673,12 @@ packages: eslint-config-riot: 1.0.0 dev: false + /@n8n_io/riot-tmpl@4.0.1: + resolution: {integrity: sha512-/zdRbEfTFjsm1NqnpPQHgZTkTdbp5v3VUxGeMA9098sps8jRCTraQkc3AQstJgHUm7ylBXJcIVhnVeLUMWAfwQ==} + dependencies: + eslint-config-riot: 1.0.0 + dev: false + /@ndelangen/get-tarball@3.0.7: resolution: {integrity: sha512-NqGfTZIZpRFef1GoVaShSSRwDC3vde3ThtTeqFdcYd6ipKqnfEVhjK2hUeHjCQUcptyZr2TONqcloFXM+5QBrQ==} dependencies: @@ -8890,7 +8909,6 @@ packages: is-nan: 1.3.2 object-is: 1.1.5 util: 0.12.5 - dev: true /assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} @@ -8919,7 +8937,6 @@ packages: engines: {node: '>=4'} dependencies: tslib: 2.6.1 - dev: true /astral-regex@2.0.0: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} @@ -11483,7 +11500,6 @@ packages: /es6-object-assign@1.1.0: resolution: {integrity: sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==} - dev: true /es6-symbol@3.1.3: resolution: {integrity: sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==} @@ -18637,7 +18653,6 @@ packages: esprima: 4.0.1 source-map: 0.6.1 tslib: 2.6.1 - dev: true /recast@0.23.3: resolution: {integrity: sha512-HbCVFh2ANP6a09nzD4lx7XthsxMOJWKX5pIcUwtLrmeEIl3I0DwjCoVXDE0Aobk+7k/mS3H50FK4iuYArpcT6Q==}