n8n/packages/workflow/test/ExpressionExtensions/helpers.ts
Mike Repeć e383f0f903
chore(core): Backport expression isolation phases 4A–8 to 1.x (no-changelog)
Completes the CAT-2844 backport of Expression Isolation to 1.x. Phases 1–3
landed via #31186/#31383/#31384; this squashes the remaining phases (whose
per-phase PRs #31385/#31387/#31388/#31389/#31390/#31474 were merged into the
stacked branches rather than 1.x):

- 4A — dispatch Expression.evaluate() to the VM evaluator (#31385)
- 4B — init/dispose VM engine in cli base command (#31387)
- 4C — acquire/release isolate at production callsites (#31388)
- 5  — ExpressionObservabilityProvider (#31389)
- 7  — workflow engine-parity test workspace (#31390)
- 8  — rebuild isolated-vm in Docker image for musl libc (#31474)
- test: make 1.x jmespath/array-proxy tests engine-aware

Engine remains opt-in via N8N_EXPRESSION_ENGINE=vm (default legacy); v1
behaviour unchanged unless enabled.

Refs https://linear.app/n8n/issue/CAT-2844
2026-06-02 12:07:42 +02:00

78 lines
2.3 KiB
TypeScript

import { DateTime, Duration, Interval } from 'luxon';
import { afterAll, beforeAll } from 'vitest';
import type { IDataObject } from '../../src/interfaces';
import { Workflow } from '../../src/workflow';
import * as Helpers from '../helpers';
export const nodeTypes = Helpers.NodeTypes();
export const workflow = new Workflow({
nodes: [
{
name: 'node',
typeVersion: 1,
type: 'test.set',
id: 'uuid-1234',
position: [0, 0],
parameters: {},
},
],
connections: {},
active: false,
nodeTypes,
});
export const expression = workflow.expression;
// acquireIsolate/releaseIsolate are no-ops for the legacy engine, so these
// hooks are safe to register unconditionally.
beforeAll(async () => {
await expression.acquireIsolate();
});
afterAll(async () => {
await expression.releaseIsolate();
});
export const evaluate = (value: string, values?: IDataObject[]) =>
expression.getParameterValue(
value,
null,
0,
0,
'node',
values?.map((v) => ({ json: v })) ?? [],
'manual',
{},
);
/**
* Normalize expression results that may be Luxon instances (legacy engine)
* or ISO strings (VM engine). The VM engine serializes Luxon types to ISO
* strings at the isolate boundary.
*/
export const asDateTime = (v: unknown): DateTime => {
if (DateTime.isDateTime(v)) return v;
if (typeof v !== 'string') throw new Error(`Expected DateTime or ISO string, got ${typeof v}`);
return DateTime.fromISO(v);
};
export const asDuration = (v: unknown): Duration => {
if (Duration.isDuration(v)) return v;
if (typeof v !== 'string') throw new Error(`Expected Duration or ISO string, got ${typeof v}`);
return Duration.fromISO(v);
};
export const asInterval = (v: unknown): Interval => {
if (Interval.isInterval(v)) return v;
if (typeof v !== 'string') throw new Error(`Expected Interval or ISO string, got ${typeof v}`);
return Interval.fromISO(v);
};
export const getLocalISOString = (date: Date) => {
const offset = date.getTimezoneOffset();
const offsetAbs = Math.abs(offset);
const isoString = new Date(date.getTime() - offset * 60 * 1000).toISOString();
const hours = String(Math.floor(offsetAbs / 60)).padStart(2, '0');
const minutes = String(offsetAbs % 60).padStart(2, '0');
return `${isoString.slice(0, -1)}${offset > 0 ? '-' : '+'}${hours}:${minutes}`;
};