first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,274 @@
// @vitest-environment jsdom
import { evaluate } from './helpers';
import { arrayExtensions } from '../../src/extensions/array-extensions';
describe('Data Transformation Functions', () => {
describe('Array Data Transformation Functions', () => {
test('.randomItem() should work correctly on an array', () => {
expect(evaluate('={{ [1,2,3].randomItem() }}')).not.toBeUndefined();
});
test('.isNotEmpty() should work correctly on an array', () => {
expect(evaluate('={{ [1,2,3, "imhere"].isNotEmpty() }}')).toEqual(true);
});
test('.pluck() should work correctly on an array', () => {
expect(
evaluate(`={{ [
{ value: 1, string: '1' },
{ value: 2, string: '2' },
{ value: 3, string: '3' },
{ value: 4, string: '4' },
{ value: 5, string: '5' },
{ value: 6, string: '6' },
{ value: { something: 'else' } }
].pluck("value") }}`),
).toEqual(expect.arrayContaining([1, 2, 3, 4, 5, 6, { something: 'else' }]));
});
test('.pluck() should work correctly for multiple values', () => {
expect(
evaluate(`={{ [
{
firstName: 'John',
lastName: 'Doe',
phone: {
home: '111-222',
office: '333-444'
}
},
{
firstName: 'Jane',
lastName: 'Doe',
phone: {
office: '555-666'
}
}
].pluck("firstName", "lastName") }}`),
).toEqual(
expect.arrayContaining([
['John', 'Doe'],
['Jane', 'Doe'],
]),
);
});
test('.pluck() should work return everything with no args', () => {
expect(
evaluate(`={{ [
{ value: 1, string: '1' },
{ value: 2, string: '2' },
{ value: 3, string: '3' },
{ value: 4, string: '4' },
{ value: 5, string: '5' },
{ value: 6, string: '6' },
{ value: { something: 'else' } }
].pluck() }}`),
).toEqual(
expect.arrayContaining([
{ value: 1, string: '1' },
{ value: 2, string: '2' },
{ value: 3, string: '3' },
{ value: 4, string: '4' },
{ value: 5, string: '5' },
{ value: 6, string: '6' },
{ value: { something: 'else' } },
]),
);
});
test('.unique() should work correctly on an array', () => {
expect(evaluate('={{ ["repeat","repeat","a","b","c"].unique() }}')).toEqual(
expect.arrayContaining(['repeat', 'repeat', 'a', 'b', 'c']),
);
});
test('.unique() should work on an arrays containing nulls, objects and arrays', () => {
expect(
evaluate('={{ [1, 2, 3, "as", {}, {}, 1, 2, [1,2], "[sad]", "[sad]", null].unique() }}'),
).toEqual([1, 2, 3, 'as', {}, [1, 2], '[sad]', null]);
});
test('.unique() should work on an arrays of objects', () => {
expect(
evaluate(
"={{ [{'name':'Nathan', age:42}, {'name':'Jan', age:16}, {'name':'Nathan', age:21}].unique('name') }}",
),
).toEqual([
{ name: 'Nathan', age: 42 },
{ name: 'Jan', age: 16 },
]);
});
test('.isEmpty() should work correctly on an array', () => {
expect(evaluate('={{ [].isEmpty() }}')).toEqual(true);
});
test('.isEmpty() should work correctly on an array', () => {
expect(evaluate('={{ [1].isEmpty() }}')).toEqual(false);
});
test('.last() should work correctly on an array', () => {
expect(evaluate('={{ ["repeat","repeat","a","b","c"].last() }}')).toEqual('c');
});
test('.first() should work correctly on an array', () => {
expect(evaluate('={{ ["repeat","repeat","a","b","c"].first() }}')).toEqual('repeat');
});
test('.merge() should work correctly on an array', () => {
expect(
evaluate(
'={{ [{ test1: 1, test2: 2 }, { test1: 1, test3: 3 }].merge([{ test1: 2, test3: 3 }, { test4: 4 }]) }}',
),
).toEqual({ test1: 1, test2: 2, test3: 3, test4: 4 });
});
test('.merge() should work correctly without arguments', () => {
expect(
evaluate(
'={{ [{ a: 1, some: null }, { a: 2, c: "something" }, 2, "asds", { b: 23 }, null, [1, 2]].merge() }}',
),
).toEqual({ a: 1, some: null, c: 'something', b: 23 });
});
test('.smartJoin() should work correctly on an array of objects', () => {
expect(
evaluate(
'={{ [{ name: "test1", value: "value1" }, { name: "test2", value: null }].smartJoin("name", "value") }}',
),
).toEqual({
test1: 'value1',
test2: null,
});
});
test('.renameKeys() should work correctly on an array of objects', () => {
expect(
evaluate(
'={{ [{ test1: 1, test2: 2 }, { test1: 1, test3: 3 }].renameKeys("test1", "rename1", "test3", "rename3") }}',
),
).toEqual([
{ rename1: 1, test2: 2 },
{ rename1: 1, rename3: 3 },
]);
});
test('.sum() should work on an array of numbers', () => {
expect(evaluate('={{ [1, 2, 3, 4, 5, 6].sum() }}')).toEqual(21);
expect(() => evaluate('={{ ["1", 2, 3, 4, 5, "bad"].sum() }}')).toThrow();
});
test('.average() should work on an array of numbers', () => {
expect(evaluate('={{ [1, 2, 3, 4, 5, 6].average() }}')).toEqual(3.5);
expect(() => evaluate('={{ ["1", 2, 3, 4, 5, "bad"].average() }}')).toThrow();
});
test('.min() should work on an array of numbers', () => {
expect(evaluate('={{ [1, 2, 3, 4, 5, 6].min() }}')).toEqual(1);
expect(() => evaluate('={{ ["1", 2, 3, 4, 5, "bad"].min() }}')).toThrow();
});
test('.max() should work on an array of numbers', () => {
expect(evaluate('={{ [1, 2, 3, 4, 5, 6].max() }}')).toEqual(6);
expect(() => evaluate('={{ ["1", 2, 3, 4, 5, "bad"].max() }}')).toThrow();
});
test('.union() should work on an array of objects', () => {
expect(
evaluate(
'={{ [{ test1: 1 }, { test2: 2 }].union([{ test1: 1, test3: 3 }, { test2: 2 }, { test4: 4 }]) }}',
),
).toEqual([{ test1: 1 }, { test2: 2 }, { test1: 1, test3: 3 }, { test4: 4 }]);
});
test('.union() should work on an arrays containing nulls, objects and arrays', () => {
expect(evaluate('={{ [1, 2, "dd", {}, null].union([1, {}, null, 3]) }}')).toEqual([
1,
2,
'dd',
{},
null,
3,
]);
});
test('.intersection() should work on an array of objects', () => {
expect(
evaluate(
'={{ [{ test1: 1 }, { test2: 2 }].intersection([{ test1: 1, test3: 3 }, { test2: 2 }, { test4: 4 }]) }}',
),
).toEqual([{ test2: 2 }]);
});
test('.intersection() should work on an arrays containing nulls, objects and arrays', () => {
expect(evaluate('={{ [1, 2, "dd", {}, null].intersection([1, {}, null]) }}')).toEqual([
1,
{},
null,
]);
});
test('.difference() should work on an array of objects', () => {
expect(
evaluate(
'={{ [{ test1: 1 }, { test2: 2 }].difference([{ test1: 1, test3: 3 }, { test2: 2 }, { test4: 4 }]) }}',
),
).toEqual([{ test1: 1 }]);
expect(
evaluate('={{ [{ test1: 1 }, { test2: 2 }].difference([{ test1: 1 }, { test2: 2 }]) }}'),
).toEqual([]);
});
test('.difference() should work on an arrays containing nulls, objects and arrays', () => {
expect(
evaluate('={{ [1, 2, "dd", {}, null, ["a", 1]].difference([1, {}, null, ["a", 1]]) }}'),
).toEqual([2, 'dd']);
});
test('.compact() should work on an array', () => {
expect(
evaluate(
'={{ [{ test1: 1, test2: undefined, test3: null }, null, undefined, 1, 2, 0, { test: "asdf" }].compact() }}',
),
).toEqual([{ test1: 1 }, 1, 2, 0, { test: 'asdf' }]);
});
test('.chunk() should work on an array', () => {
expect(evaluate('={{ numberList(1, 20).chunk(5) }}')).toEqual([
[1, 2, 3, 4, 5],
[6, 7, 8, 9, 10],
[11, 12, 13, 14, 15],
[16, 17, 18, 19, 20],
]);
});
test('.toJsonString() should work on an array', () => {
expect(evaluate('={{ [true, 1, "one", {foo: "bar"}].toJsonString() }}')).toEqual(
'[true,1,"one",{"foo":"bar"}]',
);
});
test('.append() should work on an array', () => {
expect(evaluate('={{ [1,2,3].append(4,5,"done") }}')).toEqual([1, 2, 3, 4, 5, 'done']);
});
describe('Conversion methods', () => {
test('should exist but return undefined (to not break expressions with mixed data)', () => {
expect(evaluate('={{ numberList(1, 20).toInt() }}')).toBeUndefined();
expect(evaluate('={{ numberList(1, 20).toFloat() }}')).toBeUndefined();
expect(evaluate('={{ numberList(1, 20).toBoolean() }}')).toBeUndefined();
expect(evaluate('={{ numberList(1, 20).toDateTime() }}')).toBeUndefined();
});
test('should not have a doc (hidden from autocomplete)', () => {
expect(arrayExtensions.functions.toInt.doc).toBeUndefined();
expect(arrayExtensions.functions.toFloat.doc).toBeUndefined();
expect(arrayExtensions.functions.toBoolean.doc).toBeUndefined();
expect(arrayExtensions.functions.toDateTime.doc).toBeUndefined();
});
});
});
});
@@ -0,0 +1,37 @@
// @vitest-environment jsdom
import { evaluate } from './helpers';
import { booleanExtensions } from '../../src/extensions/boolean-extensions';
describe('Data Transformation Functions', () => {
describe('Boolean Data Transformation Functions', () => {
describe('Conversion methods', () => {
describe('toInt/toFloat', () => {
test('should return 1 for true, 0 for false', () => {
expect(evaluate('={{ (true).toInt() }}')).toEqual(1);
expect(evaluate('={{ (true).toFloat() }}')).toEqual(1);
expect(evaluate('={{ (false).toInt() }}')).toEqual(0);
expect(evaluate('={{ (false).toFloat() }}')).toEqual(0);
});
});
describe('toDateTime', () => {
test('should return undefined', () => {
expect(evaluate('={{ (true).toDateTime() }}')).toBeUndefined();
});
});
describe('toBoolean', () => {
test('should return itself', () => {
expect(evaluate('={{ (true).toDateTime() }}')).toBeUndefined();
});
});
test('should not have a doc (hidden from autocomplete)', () => {
expect(booleanExtensions.functions.toFloat.doc).toBeUndefined();
expect(booleanExtensions.functions.toBoolean.doc).toBeUndefined();
expect(booleanExtensions.functions.toDateTime.doc).toBeUndefined();
});
});
});
});
@@ -0,0 +1,379 @@
// @vitest-environment jsdom
import { DateTime } from 'luxon';
import { evaluate, getLocalISOString } from './helpers';
import { dateExtensions } from '../../src/extensions/date-extensions';
import { getGlobalState } from '../../src/global-state';
const { defaultTimezone } = getGlobalState();
describe('Data Transformation Functions', () => {
describe('Date Data Transformation Functions', () => {
test('.isWeekend() should work correctly on a date', () => {
expect(evaluate('={{ DateTime.local(2023, 1, 20).isWeekend() }}')).toBe(false);
expect(evaluate('={{ DateTime.local(2023, 1, 21).isWeekend() }}')).toBe(true);
expect(evaluate('={{ DateTime.local(2023, 1, 22).isWeekend() }}')).toBe(true);
expect(evaluate('={{ DateTime.local(2023, 1, 23).isWeekend() }}')).toBe(false);
});
describe('.beginningOf', () => {
test('.beginningOf("week") should work correctly on a date', () => {
expect(evaluate('={{ DateTime.local(2023, 1, 20).beginningOf("week") }}')).toEqual(
DateTime.local(2023, 1, 16, { zone: defaultTimezone }),
);
expect(evaluate('={{ new Date(2023, 0, 20).beginningOf("week") }}')).toEqual(
DateTime.local(2023, 1, 16, { zone: defaultTimezone }).toJSDate(),
);
});
test('.beginningOf("week") should work correctly on a string', () => {
const evaluatedDate = evaluate('={{ "2023-01-30".toDate().beginningOf("week") }}');
const expectedDate = DateTime.local(2023, 1, 23, { zone: defaultTimezone }).toJSDate();
if (evaluatedDate && evaluatedDate instanceof Date) {
expect(evaluatedDate.toDateString()).toEqual(expectedDate.toDateString());
}
});
test('.beginningOf("month") should work correctly on a string', () => {
const evaluatedDate = evaluate('={{ "2023-06-16".toDate().beginningOf("month") }}');
const expectedDate = DateTime.local(2023, 6, 1, { zone: defaultTimezone }).toJSDate();
if (evaluatedDate && evaluatedDate instanceof Date) {
expect(evaluatedDate.toDateString()).toEqual(expectedDate.toDateString());
}
});
test('.beginningOf("year") should work correctly on a string', () => {
const evaluatedDate = evaluate('={{ "2023-01-30".toDate().beginningOf("year") }}');
const expectedDate = DateTime.local(2023, 1, 1, { zone: defaultTimezone }).toJSDate();
if (evaluatedDate && evaluatedDate instanceof Date) {
expect(evaluatedDate.toDateString()).toEqual(expectedDate.toDateString());
}
});
});
test('.endOfMonth() should work correctly on a date', () => {
expect(evaluate('={{ DateTime.local(2023, 1, 16).endOfMonth() }}')).toEqual(
DateTime.local(2023, 1, 31, 23, 59, 59, 999, { zone: defaultTimezone }),
);
expect(evaluate('={{ new Date(2023, 0, 16).endOfMonth() }}')).toEqual(
DateTime.local(2023, 1, 31, 23, 59, 59, 999, { zone: defaultTimezone }).toJSDate(),
);
});
describe('.extract', () => {
test('.extract("day") should work correctly on a date', () => {
expect(evaluate('={{ DateTime.local(2023, 1, 20).extract("day") }}')).toEqual(20);
});
test('should extract year from a date', () => {
expect(evaluate('={{ new Date("2024-03-30T18:49").extract("year") }}')).toEqual(2024);
});
test('should extract yearDayNumber from a date', () => {
expect(evaluate('={{ new Date("2024-03-30T18:49").extract("yearDayNumber") }}')).toEqual(
90,
);
});
test('should extract month from a date', () => {
expect(evaluate('={{ new Date("2024-03-30T18:49").extract("month") }}')).toEqual(3);
});
test('should extract week from a date', () => {
expect(evaluate('={{ DateTime.local(2023, 1, 20).extract() }}')).toEqual(3);
expect(evaluate('={{ DateTime.local(2023, 1, 20).extract("week") }}')).toEqual(3);
});
test('should extract day from a date', () => {
expect(evaluate('={{ DateTime.fromISO("2024-03-30T18:49").extract("day") }}')).toEqual(30);
});
test('should extract hour from a date', () => {
expect(evaluate('={{ DateTime.fromISO("2024-03-30T18:49").extract("hour") }}')).toEqual(18);
});
test('should extract minute from a date', () => {
expect(evaluate('={{ DateTime.fromISO("2024-03-30T18:49").extract("minute") }}')).toEqual(
49,
);
});
test('should extract second from a date', () => {
expect(evaluate('={{ DateTime.fromISO("2024-03-30T18:49").extract("second") }}')).toEqual(
0,
);
});
test('should extract millisecond from a date', () => {
expect(
evaluate('={{ DateTime.fromISO("2024-03-30T18:49:00.123Z").extract("millisecond") }}'),
).toEqual(123);
});
test('should return undefined for invalid unit', () => {
expect(evaluate('={{ DateTime.fromISO("2024-03-30T18:49").extract("invalid") }}')).toBe(
null,
);
});
});
describe('.format', () => {
test('should format date with custom format', () => {
expect(
evaluate('={{ DateTime.fromISO("2024-03-30T18:49").format("yyyy LLL dd") }}'),
).toEqual('2024 Mar 30');
});
test('should format date with ISO format', () => {
expect(
evaluate('={{ DateTime.fromISO("2024-03-30T18:49").format("yyyy-MM-dd\'T\'HH:mm:ss") }}'),
).toEqual('2024-03-30T18:49:00');
});
});
test('.toDate() should work on a string', () => {
const date = new Date(2022, 0, 3);
expect(evaluate(`={{ "${getLocalISOString(date)}".toDate() }}`)).toEqual(date);
});
describe('.inBetween', () => {
test('should work on string and Date', () => {
expect(
evaluate("={{ $now.isBetween('2023-06-23'.toDate(), '2023-06-23') }}"),
).toBeDefined();
});
test('should work on string and DateTime', () => {
expect(evaluate("={{ $now.isBetween($now, '2023-06-23') }}")).toBeDefined();
});
test('should not work for invalid strings', () => {
expect(evaluate("={{ $now.isBetween($now, 'invalid') }}")).toBeUndefined();
});
test('should not work for numbers', () => {
expect(evaluate('={{ $now.isBetween($now, 1) }}')).toBeUndefined();
});
test('should not work for a single argument', () => {
expect(() => evaluate('={{ $now.isBetween($now) }}')).toThrow();
});
test('should not work for a more than two arguments', () => {
expect(() =>
evaluate("={{ $now.isBetween($now, '2023-06-23', '2023-09-21'.toDate()) }}"),
).toThrow();
});
});
describe('.diffTo', () => {
test('should work with a single unit', () => {
expect(
evaluate(
"={{ '2025-01-01'.toDateTime().diffTo('2024-03-30T18:49:07.234', 'days').floor() }}",
),
).toEqual(276);
});
test('should work with an array of units', () => {
expect(
evaluate(
"={{ '2025-01-01T00:00:00.000'.toDateTime().diffTo('2024-03-30T18:49:07.234', ['months', 'days']) }}",
),
).toEqual({ months: 9, days: 1.2158884953703704 });
});
test('should return difference in days', () => {
expect(
evaluate(
'={{ DateTime.fromISO("2024-03-30T18:49:00Z").diffTo("2024-03-25T18:49:00Z", "days") }}',
),
).toEqual(5);
});
test('should return difference in hours', () => {
expect(
evaluate(
'={{ DateTime.fromISO("2024-03-30T18:49:00Z").diffTo("2024-03-30T12:49:00Z", "hours") }}',
),
).toEqual(6);
});
test('should return difference in minutes', () => {
expect(
evaluate(
'={{ DateTime.fromISO("2024-03-30T18:49:00Z").diffTo("2024-03-30T18:44:00Z", "minutes") }}',
),
).toEqual(5);
});
test('should return difference in seconds', () => {
expect(
evaluate(
'={{ DateTime.fromISO("2024-03-30T18:49:00Z").diffTo("2024-03-30T18:48:55Z", "seconds") }}',
),
).toEqual(5);
});
test('should return difference in milliseconds', () => {
expect(
evaluate(
'={{ DateTime.fromISO("2024-03-30T18:49:00.500Z").diffTo("2024-03-30T18:49:00.000Z", "milliseconds") }}',
),
).toEqual(500);
});
test('should throw for invalid unit', () => {
expect(() =>
evaluate(
'={{ DateTime.fromISO("2024-03-30T18:49:00Z").diffTo("2024-03-30T18:49:00Z", "invalid") }}',
),
).toThrow('Unsupported unit');
});
});
describe('.toDateTime', () => {
test('should return itself for DateTime', () => {
const result = evaluate(
"={{ DateTime.fromFormat('01-01-2024', 'dd-MM-yyyy').toDateTime() }}",
) as unknown as DateTime;
expect(result).toBeInstanceOf(DateTime);
expect(result.day).toEqual(1);
expect(result.month).toEqual(1);
expect(result.year).toEqual(2024);
});
test('should return a DateTime for JS Date', () => {
const result = evaluate(
'={{ new Date(2024, 0, 1, 12).toDateTime() }}',
) as unknown as DateTime;
expect(result).toBeInstanceOf(DateTime);
expect(result.day).toEqual(1);
expect(result.month).toEqual(1);
expect(result.year).toEqual(2024);
});
});
describe('.toInt/.toFloat', () => {
test('should return milliseconds for DateTime', () => {
expect(evaluate("={{ DateTime.fromISO('2024-01-01T00:00:00.000Z').toInt() }}")).toEqual(
1704067200000,
);
});
test('should return milliseconds for JS Date', () => {
expect(evaluate('={{ new Date("2024-01-01T00:00:00.000Z").toFloat() }}')).toEqual(
1704067200000,
);
});
test('should not have a doc (hidden from autocomplete)', () => {
expect(dateExtensions.functions.toInt.doc).toBeUndefined();
expect(dateExtensions.functions.toFloat.doc).toBeUndefined();
});
});
describe('.toBoolean', () => {
test('should return undefined', () => {
expect(evaluate('={{ new Date("2024-01-01T00:00:00.000Z").toBoolean() }}')).toBeUndefined();
});
test('should not have a doc (hidden from autocomplete)', () => {
expect(dateExtensions.functions.toBoolean.doc).toBeUndefined();
});
});
describe('.isInLast', () => {
it('should return true if the date is within the last n minutes', () => {
expect(
evaluate(
`={{ new Date("${DateTime.now().minus({ minutes: 5 }).toISO()}").isInLast(10, "minutes") }}`,
),
).toBe(true);
});
it('should return false if the date is not within the last n minutes', () => {
expect(
evaluate(
`={{ new Date("${DateTime.now().minus({ minutes: 15 }).toISO()}").isInLast(10, "minutes") }}`,
),
).toBe(false);
});
it('should handle default unit as minutes', () => {
expect(
evaluate(
`={{ new Date("${DateTime.now().minus({ minutes: 5 }).toISO()}").isInLast(10) }}`,
),
).toBe(true);
});
});
describe('.minus', () => {
it('should subtract days from the date', () => {
expect(evaluate('={{ new Date("2024-03-30T18:49:00Z").minus(7, "days") }}')).toEqual(
new Date('2024-03-23T18:49:00.000Z'),
);
});
it('should subtract years from the date', () => {
expect(evaluate('={{ new Date("2024-03-30T18:49:00Z").minus(4, "years") }}')).toEqual(
new Date('2020-03-30T18:49:00.000Z'),
);
});
it('should handle default unit as milliseconds', () => {
expect(evaluate('={{ new Date("2024-03-30T18:49:00Z").minus(1000) }}')).toEqual(
new Date('2024-03-30T18:48:59.000Z'),
);
});
it('should handle DateTime instances', () => {
expect(
evaluate('={{ DateTime.fromISO("2024-03-30T18:49:00Z").minus(1, "day").toJSDate() }}'),
).toEqual(new Date('2024-03-29T18:49:00.000Z'));
});
});
describe('.plus', () => {
it('should subtract days from the date', () => {
expect(evaluate('={{ new Date("2024-03-30T18:49:00Z").plus(7, "days") }}')).toEqual(
new Date('2024-04-06T18:49:00.000Z'),
);
});
it('should subtract years from the date', () => {
expect(evaluate('={{ new Date("2024-03-30T18:49:00Z").plus(4, "years") }}')).toEqual(
new Date('2028-03-30T18:49:00.000Z'),
);
});
it('should handle default unit as milliseconds', () => {
expect(evaluate('={{ new Date("2024-03-30T18:49:00Z").plus(1000) }}')).toEqual(
new Date('2024-03-30T18:49:01.000Z'),
);
});
it('should handle DateTime instances', () => {
expect(
evaluate('={{ DateTime.fromISO("2024-03-30T18:49:00Z").plus(1, "day").toJSDate() }}'),
).toEqual(new Date('2024-03-31T18:49:00.000Z'));
});
});
describe('.isDst', () => {
test('should return true for a date in DST', () => {
expect(evaluate('={{ DateTime.fromISO("2024-06-30T18:49:00Z").isDst() }}')).toBe(true);
});
test('should return false for a date not in DST', () => {
expect(evaluate('={{ DateTime.fromISO("2024-01-30T18:49:00Z").isDst() }}')).toBe(false);
});
});
});
});
@@ -0,0 +1,266 @@
// @vitest-environment jsdom
/* eslint-disable n8n-local-rules/no-interpolation-in-regular-string */
import { evaluate } from './helpers';
import { ExpressionExtensionError } from '../../src/errors/expression-extension.error';
import { extendTransform, extend } from '../../src/extensions';
import { joinExpression, splitExpression } from '../../src/extensions/expression-parser';
describe('Expression Extension Transforms', () => {
describe('extend() transform', () => {
test('Basic transform with .isEmpty', () => {
expect(extendTransform('"".isEmpty()')!.code).toEqual('extend("", "isEmpty", [])');
});
test('Chained transform with .toSnakeCase.toSentenceCase', () => {
expect(extendTransform('"".toSnakeCase().toSentenceCase(2)')!.code).toEqual(
'extend(extend("", "toSnakeCase", []), "toSentenceCase", [2])',
);
});
test('Chained transform with native functions .toSnakeCase.trim.toSentenceCase', () => {
expect(extendTransform('"aaa ".toSnakeCase().trim().toSentenceCase(2)')!.code).toEqual(
'extend(extend("aaa ", "toSnakeCase", []).trim(), "toSentenceCase", [2])',
);
});
});
});
describe('Expression Parser', () => {
describe('Compatible splitting', () => {
test('Lone expression', () => {
expect(splitExpression('{{ "" }}')).toEqual([
{ type: 'text', text: '' },
{ type: 'code', text: ' "" ', hasClosingBrackets: true },
]);
});
test('Multiple expression', () => {
expect(splitExpression('{{ "test".toSnakeCase() }} you have ${{ (100).format() }}.')).toEqual(
[
{ type: 'text', text: '' },
{ type: 'code', text: ' "test".toSnakeCase() ', hasClosingBrackets: true },
{ type: 'text', text: ' you have $' },
{ type: 'code', text: ' (100).format() ', hasClosingBrackets: true },
{ type: 'text', text: '.' },
],
);
});
test('Unclosed expression', () => {
expect(splitExpression('{{ "test".toSnakeCase() }} you have ${{ (100).format()')).toEqual([
{ type: 'text', text: '' },
{ type: 'code', text: ' "test".toSnakeCase() ', hasClosingBrackets: true },
{ type: 'text', text: ' you have $' },
{ type: 'code', text: ' (100).format()', hasClosingBrackets: false },
]);
});
test('Escaped opening bracket', () => {
expect(splitExpression('test \\{{ no code }}')).toEqual([
{ type: 'text', text: 'test \\{{ no code }}' },
]);
});
test('Escaped closinging bracket', () => {
expect(splitExpression('test {{ code.test("\\}}") }}')).toEqual([
{ type: 'text', text: 'test ' },
{ type: 'code', text: ' code.test("}}") ', hasClosingBrackets: true },
]);
});
});
describe('Compatible joining', () => {
test('Lone expression', () => {
expect(joinExpression(splitExpression('{{ "" }}'))).toEqual('{{ "" }}');
});
test('Multiple expression', () => {
expect(
joinExpression(
splitExpression('{{ "test".toSnakeCase() }} you have ${{ (100).format() }}.'),
),
).toEqual('{{ "test".toSnakeCase() }} you have ${{ (100).format() }}.');
});
test('Unclosed expression', () => {
expect(
joinExpression(splitExpression('{{ "test".toSnakeCase() }} you have ${{ (100).format()')),
).toEqual('{{ "test".toSnakeCase() }} you have ${{ (100).format()');
});
test('Escaped opening bracket', () => {
expect(joinExpression(splitExpression('test \\{{ no code }}'))).toEqual(
'test \\{{ no code }}',
);
});
test('Escaped closing bracket', () => {
expect(joinExpression(splitExpression('test {{ code.test("\\}}") }}'))).toEqual(
'test {{ code.test("\\}}") }}',
);
});
});
describe('Edge cases', () => {
test("Nested member access with name of function inside a function doesn't result in function call", () => {
expect(evaluate('={{ Math.floor([1, 2, 3, 4].length + 10) }}')).toEqual(14);
expect(extendTransform('Math.floor([1, 2, 3, 4].length + 10)')?.code).toBe(
'extend(Math, "floor", [[1, 2, 3, 4].length + 10])',
);
});
});
describe('Test newer ES syntax', () => {
test('Optional chaining transforms', () => {
expect(extendTransform('$json.something?.test.funcCall()')?.code).toBe(
'window.chainCancelToken1 = ((window.chainValue1 = $json.something) ?? undefined) === undefined, window.chainCancelToken1 === true ? undefined : window.chainValue1.test.funcCall();',
);
expect(extendTransform('$json.something?.test.funcCall()?.somethingElse')?.code).toBe(
'window.chainCancelToken1 = ((window.chainValue1 = $json.something) ?? undefined) === undefined, window.chainCancelToken1 === true ? undefined : window.chainCancelToken1 = ((window.chainValue1 = window.chainValue1.test.funcCall()) ?? undefined) === undefined, window.chainCancelToken1 === true ? undefined : window.chainValue1.somethingElse;',
);
expect(extendTransform('$json.something?.test.funcCall().somethingElse')?.code).toBe(
'window.chainCancelToken1 = ((window.chainValue1 = $json.something) ?? undefined) === undefined, window.chainCancelToken1 === true ? undefined : window.chainValue1.test.funcCall().somethingElse;',
);
expect(
extendTransform('$json.something?.test.funcCall()?.somethingElse.otherCall()')?.code,
).toBe(
'window.chainCancelToken1 = ((window.chainValue1 = $json.something) ?? undefined) === undefined, window.chainCancelToken1 === true ? undefined : window.chainCancelToken1 = ((window.chainValue1 = window.chainValue1.test.funcCall()) ?? undefined) === undefined, window.chainCancelToken1 === true ? undefined : window.chainValue1.somethingElse.otherCall();',
);
expect(evaluate('={{ [1, 2, 3, 4]?.sum() }}')).toBe(10);
});
test('Optional chaining transforms on calls', () => {
expect(extendTransform('Math.min?.(1)')?.code).toBe(
'window.chainCancelToken1 = ((window.chainValue1 = extendOptional(Math, "min")) ?? undefined) === undefined, window.chainCancelToken1 === true ? undefined : window.chainValue1(1);',
);
expect(extendTransform('Math?.min?.(1)')?.code).toBe(
'window.chainCancelToken1 = ((window.chainValue1 = Math) ?? undefined) === undefined, window.chainCancelToken1 === true ? undefined : window.chainCancelToken1 = ((window.chainValue1 = extendOptional(window.chainValue1, "min")) ?? undefined) === undefined, window.chainCancelToken1 === true ? undefined : window.chainValue1(1);',
);
expect(extendTransform('$json.test.test2?.sum()')?.code).toBe(
'window.chainCancelToken1 = ((window.chainValue1 = $json.test.test2) ?? undefined) === undefined, window.chainCancelToken1 === true ? undefined : extend(window.chainValue1, "sum", []);',
);
expect(extendTransform('$json.test.test2?.sum?.()')?.code).toBe(
'window.chainCancelToken1 = ((window.chainValue1 = $json.test.test2) ?? undefined) === undefined, window.chainCancelToken1 === true ? undefined : window.chainCancelToken1 = ((window.chainValue1 = extendOptional(window.chainValue1, "sum")) ?? undefined) === undefined, window.chainCancelToken1 === true ? undefined : window.chainValue1();',
);
expect(evaluate('={{ [1, 2, 3, 4].sum?.() }}')).toBe(10);
});
test('Multiple optional chains in an expression', () => {
expect(extendTransform('$json.test?.test2($json.test?.test2)')?.code).toBe(`window.chainCancelToken2 = ((window.chainValue2 = $json.test) ?? undefined) === undefined, window.chainCancelToken2 === true ? undefined : window.chainValue2.test2(
(window.chainCancelToken1 = ((window.chainValue1 = $json.test) ?? undefined) === undefined, window.chainCancelToken1 === true ? undefined : window.chainValue1.test2)
);`);
expect(extendTransform('$json.test?.test2($json.test.sum?.())')?.code).toBe(`window.chainCancelToken2 = ((window.chainValue2 = $json.test) ?? undefined) === undefined, window.chainCancelToken2 === true ? undefined : window.chainValue2.test2(
(window.chainCancelToken1 = ((window.chainValue1 = extendOptional($json.test, "sum")) ?? undefined) === undefined, window.chainCancelToken1 === true ? undefined : window.chainValue1())
);`);
});
expect(evaluate('={{ [1, 2, 3, 4]?.sum((undefined)?.test) }}')).toBe(10);
});
describe('Non dot extensions', () => {
test('min', () => {
expect(evaluate('={{ min(1, 2, 3, 4, 5, 6) }}')).toEqual(1);
expect(evaluate('={{ min(1, NaN, 3, 4, 5, 6) }}')).toBeNaN();
});
test('max', () => {
expect(evaluate('={{ max(1, 2, 3, 4, 5, 6) }}')).toEqual(6);
expect(evaluate('={{ max(1, NaN, 3, 4, 5, 6) }}')).toBeNaN();
});
test('average', () => {
expect(evaluate('={{ average(1, 2, 3, 4, 5, 6) }}')).toEqual(3.5);
expect(evaluate('={{ average(1, NaN, 3, 4, 5, 6) }}')).toBeNaN();
});
test('numberList', () => {
expect(evaluate('={{ numberList(1, 10) }}')).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
expect(evaluate('={{ numberList(1, -10) }}')).toEqual([
1, 0, -1, -2, -3, -4, -5, -6, -7, -8, -9, -10,
]);
});
test('zip', () => {
expect(evaluate('={{ zip(["test1", "test2", "test3"], [1, 2, 3]) }}')).toEqual({
test1: 1,
test2: 2,
test3: 3,
});
});
test('$if', () => {
expect(evaluate('={{ $if("a"==="a", 1, 2) }}')).toEqual(1);
expect(evaluate('={{ $if("a"==="b", 1, 2) }}')).toEqual(2);
expect(evaluate('={{ $if("a"==="a", 1) }}')).toEqual(1);
expect(evaluate('={{ $if("a"==="b", 1) }}')).toEqual(false);
// This will likely break when sandboxing is implemented but it works for now.
// If you're implementing sandboxing maybe provide a way to add functions to
// sandbox we can check instead?
const mockCallback = vi.fn(() => false);
evaluate('={{ $if("a"==="a", true, $data.cb()) }}', [{ cb: mockCallback }]);
expect(mockCallback.mock.calls.length).toEqual(0);
evaluate('={{ $if("a"==="b", true, $data.cb()) }}', [{ cb: mockCallback }]);
expect(mockCallback.mock.calls.length).toEqual(1);
});
test('$not', () => {
expect(evaluate('={{ $not(1) }}')).toEqual(false);
expect(evaluate('={{ $not(0) }}')).toEqual(true);
expect(evaluate('={{ $not(true) }}')).toEqual(false);
expect(evaluate('={{ $not(false) }}')).toEqual(true);
expect(evaluate('={{ $not(undefined) }}')).toEqual(true);
expect(evaluate('={{ $not(null) }}')).toEqual(true);
expect(evaluate('={{ $not("") }}')).toEqual(true);
expect(evaluate('={{ $not("a") }}')).toEqual(false);
});
test('$ifEmpty', () => {
expect(evaluate('={{ $ifEmpty(1, "default") }}')).toEqual(1);
expect(evaluate('={{ $ifEmpty(0, "default") }}')).toEqual(0);
expect(evaluate('={{ $ifEmpty(false, "default") }}')).toEqual(false);
expect(evaluate('={{ $ifEmpty(true, "default") }}')).toEqual(true);
expect(evaluate('={{ $ifEmpty("", "default") }}')).toEqual('default');
expect(evaluate('={{ $ifEmpty(null, "default") }}')).toEqual('default');
expect(evaluate('={{ $ifEmpty(undefined, "default") }}')).toEqual('default');
expect(evaluate('={{ $ifEmpty([], "default") }}')).toEqual('default');
expect(evaluate('={{ $ifEmpty({}, "default") }}')).toEqual('default');
expect(evaluate('={{ $ifEmpty([1], "default") }}')).toEqual([1]);
expect(evaluate('={{ $ifEmpty({a: 1}, "default") }}')).toEqual({ a: 1 });
});
});
describe('Test extend with undefined', () => {
test('input is undefined', () => {
try {
extend(undefined, 'toDateTime', []);
} catch (error) {
expect(error).toBeInstanceOf(ExpressionExtensionError);
expect(error).toHaveProperty('message', "toDateTime can't be used on undefined value");
}
});
test('input is null', () => {
try {
extend(null, 'startsWith', []);
} catch (error) {
expect(error).toBeInstanceOf(ExpressionExtensionError);
expect(error).toHaveProperty('message', "startsWith can't be used on null value");
}
});
test('input should be converted to upper case', () => {
const result = extend('TEST', 'toUpperCase', []);
expect(result).toEqual('TEST');
});
});
});
@@ -0,0 +1,9 @@
import { evaluate } from './helpers';
describe('Data Transformation Functions', () => {
describe('Genric Data Transformation Functions', () => {
test('.isEmpty() should work correctly on undefined', () => {
expect(evaluate('={{(undefined).isEmpty()}}')).toEqual(true);
});
});
});
@@ -0,0 +1,42 @@
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;
export const evaluate = (value: string, values?: IDataObject[]) =>
expression.getParameterValue(
value,
null,
0,
0,
'node',
values?.map((v) => ({ json: v })) ?? [],
'manual',
{},
);
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}`;
};
@@ -0,0 +1,118 @@
// @vitest-environment jsdom
import { evaluate } from './helpers';
import { numberExtensions } from '../../src/extensions/number-extensions';
describe('Data Transformation Functions', () => {
describe('Number Data Transformation Functions', () => {
test('.format() should work correctly on a number', () => {
expect(evaluate('={{ Number(100).format() }}')).toEqual(
numberExtensions.functions.format(100, []),
);
});
test('.ceil() should work on a number', () => {
expect(evaluate('={{ (1.2).ceil() }}')).toEqual(2);
expect(evaluate('={{ (1.9).ceil() }}')).toEqual(2);
expect(evaluate('={{ (1.0).ceil() }}')).toEqual(1);
expect(evaluate('={{ (NaN).ceil() }}')).toBeNaN();
});
test('.floor() should work on a number', () => {
expect(evaluate('={{ (1.2).floor() }}')).toEqual(1);
expect(evaluate('={{ (1.9).floor() }}')).toEqual(1);
expect(evaluate('={{ (1.0).floor() }}')).toEqual(1);
expect(evaluate('={{ (NaN).floor() }}')).toBeNaN();
});
test('.round() should work on a number', () => {
expect(evaluate('={{ (1.3333333).round(3) }}')).toEqual(1.333);
expect(evaluate('={{ (1.3333333).round(0) }}')).toEqual(1);
expect(evaluate('={{ (1.5001).round(0) }}')).toEqual(2);
expect(evaluate('={{ (NaN).round(3) }}')).toBeNaN();
});
test('.isOdd() should work on a number', () => {
expect(evaluate('={{ (9).isOdd() }}')).toEqual(true);
expect(evaluate('={{ (8).isOdd() }}')).toEqual(false);
expect(evaluate('={{ (0).isOdd() }}')).toEqual(false);
});
test('.isOdd() should not work on a float or NaN', () => {
expect(() => evaluate('={{ (NaN).isOdd() }}')).toThrow();
expect(() => evaluate('={{ (9.2).isOdd() }}')).toThrow();
});
test('.isEven() should work on a number', () => {
expect(evaluate('={{ (9).isEven() }}')).toEqual(false);
expect(evaluate('={{ (8).isEven() }}')).toEqual(true);
expect(evaluate('={{ (0).isEven() }}')).toEqual(true);
});
test('.isEven() should not work on a float or NaN', () => {
expect(() => evaluate('={{ (NaN).isEven() }}')).toThrow();
expect(() => evaluate('={{ (9.2).isEven() }}')).toThrow();
});
describe('toDateTime', () => {
test('from milliseconds (default)', () => {
expect(evaluate('={{ (1704085200000).toDateTime().toISO() }}')).toEqual(
'2024-01-01T00:00:00.000-05:00',
);
expect(evaluate('={{ (1704085200000).toDateTime("ms").toISO() }}')).toEqual(
'2024-01-01T00:00:00.000-05:00',
);
});
test('from seconds', () => {
expect(evaluate('={{ (1704085200).toDateTime("s").toISO() }}')).toEqual(
'2024-01-01T00:00:00.000-05:00',
);
});
test('from Excel 1900 format', () => {
expect(evaluate('={{ (42144).toDateTime("excel").toISO() }}')).toEqual(
'2015-05-19T20:00:00.000-04:00',
);
});
test('from microseconds', () => {
expect(evaluate('={{ (1704085200000000).toDateTime("us").toISO() }}')).toEqual(
'2024-01-01T00:00:00.000-05:00',
);
});
});
describe('toInt', () => {
test('should round numbers', () => {
expect(evaluate('={{ (42144).toInt() }}')).toEqual(42144);
expect(evaluate('={{ (42144.345).toInt() }}')).toEqual(42144);
expect(evaluate('={{ (42144.545).toInt() }}')).toEqual(42145);
});
});
describe('toFloat', () => {
test('should return itself', () => {
expect(evaluate('={{ (42144).toFloat() }}')).toEqual(42144);
expect(evaluate('={{ (42144.345).toFloat() }}')).toEqual(42144.345);
});
});
describe('toBoolean', () => {
test('should return false for 0, 1 for other numbers', () => {
expect(evaluate('={{ (42144).toBoolean() }}')).toBe(true);
expect(evaluate('={{ (-1.549).toBoolean() }}')).toBe(true);
expect(evaluate('={{ (0).toBoolean() }}')).toBe(false);
});
});
});
describe('Multiple expressions', () => {
test('Basic multiple expressions', () => {
// eslint-disable-next-line n8n-local-rules/no-interpolation-in-regular-string
expect(evaluate('={{ "test abc".toSnakeCase() }} you have ${{ (100).format() }}.')).toEqual(
'test_abc you have $100.',
);
});
});
});
@@ -0,0 +1,184 @@
import { evaluate } from './helpers';
import { ApplicationError } from '../../src/errors';
import { objectExtensions } from '../../src/extensions/object-extensions';
describe('Data Transformation Functions', () => {
describe('Object Data Transformation Functions', () => {
describe('.isEmpty', () => {
test('should return true for an empty object', () => {
expect(evaluate('={{ ({}).isEmpty() }}')).toBe(true);
});
test('should return false for a non-empty object', () => {
expect(evaluate('={{ ({ test: 1 }).isEmpty() }}')).toBe(false);
});
test('should return true for an object with only null/undefined values', () => {
expect(evaluate('={{ ({ test1: null, test2: undefined }).isEmpty() }}')).toBe(false);
});
});
describe('.hasField', () => {
test('should return true if the key exists in the object', () => {
expect(evaluate('={{ ({ test1: 1 }).hasField("test1") }}')).toBe(true);
});
test('should return false if the key does not exist in the object', () => {
expect(evaluate('={{ ({ test1: 1 }).hasField("test2") }}')).toBe(false);
});
});
test('.removeField should work on an object', () => {
expect(evaluate('={{ ({ test1: 1, test2: 2, test3: 3 }).removeField("test2") }}')).toEqual({
test1: 1,
test3: 3,
});
expect(
evaluate('={{ ({ test1: 1, test2: 2, test3: 3 }).removeField("testDoesntExist") }}'),
).toEqual({
test1: 1,
test2: 2,
test3: 3,
});
});
describe('.removeFieldsContaining', () => {
test('should work on an object', () => {
expect(
evaluate(
'={{ ({ test1: "i exist", test2: "i should be removed", test3: "i should also be removed" }).removeFieldsContaining("removed") }}',
),
).toEqual({
test1: 'i exist',
});
});
test('should not work for empty string', () => {
expect(() =>
evaluate(
'={{ ({ test1: "i exist", test2: "i should be removed", test3: "i should also be removed" }).removeFieldsContaining("") }}',
),
).toThrow();
});
});
describe('.keepFieldsContaining', () => {
test('.keepFieldsContaining should work on an object', () => {
expect(
evaluate(
'={{ ({ test1: "i exist", test2: "i should be removed", test3: "i should also be removed" }).keepFieldsContaining("exist") }}',
),
).toEqual({
test1: 'i exist',
});
});
test('.keepFieldsContaining should work on a nested object', () => {
expect(
evaluate(
'={{ ({ test1: "i exist", test2: "i should be removed", test3: { test4: "me too" } }).keepFieldsContaining("exist") }}',
),
).toEqual({
test1: 'i exist',
});
});
test('.keepFieldsContaining should not work for empty string', () => {
expect(() =>
evaluate(
'={{ ({ test1: "i exist", test2: "i should be removed", test3: "i should also be removed" }).keepFieldsContaining("") }}',
),
).toThrow();
});
});
describe('.compact', () => {
test('should work on an object', () => {
expect(
evaluate('={{ ({ test1: 1, test2: "2", test3: undefined, test4: null }).compact() }}'),
).toEqual({ test1: 1, test2: '2' });
});
test('should remove fields with null, undefined, empty string, or "nil"', () => {
expect(
evaluate(
'={{ ({ test1: 0, test2: false, test3: "", test4: "nil", test5: NaN }).compact() }}',
),
).toEqual({ test1: 0, test2: false, test5: NaN });
});
test('should work on an empty object', () => {
expect(evaluate('={{ ({}).compact() }}')).toEqual({});
});
test('should work on an object with all null/undefined values', () => {
expect(evaluate('={{ ({ test1: undefined, test2: null }).compact() }}')).toEqual({});
});
test('should work on an object with nested null/undefined values', () => {
expect(
evaluate(
'={{ ({ test1: 1, test2: { nested1: null, nested2: "value" }, test3: undefined }).compact() }}',
),
).toEqual({ test1: 1, test2: { nested2: 'value' } });
});
test('should not allow prototype pollution', () => {
['{__proto__: {polluted: true}}', '{constructor: {prototype: {polluted: true}}}'].forEach(
(testExpression) => {
expect(() => evaluate(`={{ (${testExpression}).compact() }}`)).toThrow(
ApplicationError,
);
expect(({} as any).polluted).toBeUndefined();
},
);
});
});
test('.urlEncode should work on an object', () => {
expect(evaluate('={{ ({ test1: 1, test2: "2" }).urlEncode() }}')).toEqual('test1=1&test2=2');
});
describe('.keys', () => {
test('should return an array of keys from the object', () => {
expect(evaluate('={{ ({ test1: 1, test2: 2 }).keys() }}')).toEqual(['test1', 'test2']);
});
test('should return an empty array for an empty object', () => {
expect(evaluate('={{ ({}).keys() }}')).toEqual([]);
});
});
describe('.values', () => {
test('should return an array of values from the object', () => {
expect(evaluate('={{ ({ test1: 1, test2: "value" }).values() }}')).toEqual([1, 'value']);
});
test('should return an empty array for an empty object', () => {
expect(evaluate('={{ ({}).values() }}')).toEqual([]);
});
});
test('.toJsonString() should work on an object', () => {
expect(evaluate('={{ ({ test1: 1, test2: "2" }).toJsonString() }}')).toEqual(
'{"test1":1,"test2":"2"}',
);
});
describe('Conversion methods', () => {
test('should exist but return undefined (to not break expressions with mixed data)', () => {
expect(evaluate('={{ ({ test1: 1, test2: "2" }).toInt() }}')).toBeUndefined();
expect(evaluate('={{ ({ test1: 1, test2: "2" }).toFloat() }}')).toBeUndefined();
expect(evaluate('={{ ({ test1: 1, test2: "2" }).toBoolean() }}')).toBeUndefined();
expect(evaluate('={{ ({ test1: 1, test2: "2" }).toDateTime() }}')).toBeUndefined();
});
it('should not have a doc (hidden from autocomplete)', () => {
expect(objectExtensions.functions.toInt.doc).toBeUndefined();
expect(objectExtensions.functions.toFloat.doc).toBeUndefined();
expect(objectExtensions.functions.toBoolean.doc).toBeUndefined();
expect(objectExtensions.functions.toDateTime.doc).toBeUndefined();
});
});
});
});
@@ -0,0 +1,340 @@
// @vitest-environment jsdom
import { DateTime } from 'luxon';
import { evaluate } from './helpers';
import { ExpressionExtensionError } from '../../src/errors';
describe('Data Transformation Functions', () => {
describe('String Data Transformation Functions', () => {
describe('.isEmpty', () => {
test('should work correctly on a string that is not empty', () => {
expect(evaluate('={{"NotBlank".isEmpty()}}')).toEqual(false);
});
test('should work correctly on a string that is empty', () => {
expect(evaluate('={{"".isEmpty()}}')).toEqual(true);
});
});
describe('.isNotEmpty', () => {
test('should work correctly on a string that is not empty', () => {
expect(evaluate('={{"NotBlank".isNotEmpty()}}')).toEqual(true);
});
test('should work correctly on a string that is empty', () => {
expect(evaluate('={{"".isNotEmpty()}}')).toEqual(false);
});
});
test('.length should return the string length', () => {
expect(evaluate('={{"String".length()}}')).toEqual(6);
});
describe('.hash()', () => {
test.each([
['base64', 'MTIzNDU='],
['md5', '827ccb0eea8a706c4c34a16891f84e7b'],
['sha1', '8cb2237d0679ca88db6464eac60da96345513964'],
['sha224', 'a7470858e79c282bc2f6adfd831b132672dfd1224c1e78cbf5bcd057'],
['sha256', '5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5'],
[
'sha384',
'0fa76955abfa9dafd83facca8343a92aa09497f98101086611b0bfa95dbc0dcc661d62e9568a5a032ba81960f3e55d4a',
],
[
'sha512',
'3627909a29c31381a071ec27f7c9ca97726182aed29a7ddd2e54353322cfb30abb9e3a6df2ac2c20fe23436311d678564d0c8d305930575f60e2d3d048184d79',
],
[
'sha3',
'0a2a1719bf3ce682afdbedf3b23857818d526efbe7fcb372b31347c26239a0f916c398b7ad8dd0ee76e8e388604d0b0f925d5e913ad2d3165b9b35b3844cd5e6',
],
])('should work for %p', (hashFn, hashValue) => {
expect(evaluate(`={{ "12345".hash("${hashFn}") }}`)).toEqual(hashValue);
expect(evaluate(`={{ "12345".hash("${hashFn.toLowerCase()}") }}`)).toEqual(hashValue);
});
test('should throw on invalid algorithm', () => {
expect(() => evaluate('={{ "12345".hash("invalid") }}')).toThrow('Unknown algorithm');
});
});
test('.urlDecode should work correctly on a string', () => {
expect(evaluate('={{ "string%20with%20spaces".urlDecode(false) }}')).toEqual(
'string with spaces',
);
});
test('.urlEncode should work correctly on a string', () => {
expect(evaluate('={{ "string with spaces".urlEncode(false) }}')).toEqual(
'string%20with%20spaces',
);
});
test('.removeTags should work correctly on a string', () => {
expect(evaluate('={{ "<html><head>test</head></html>".removeTags() }}')).toEqual('test');
});
test('.removeMarkdown should work correctly on a string', () => {
expect(evaluate('={{ "<html><head>test</head></html>".removeMarkdown() }}')).toEqual('test');
});
test('.toLowerCase should work correctly on a string', () => {
expect(evaluate('={{ "TEST".toLowerCase() }}')).toEqual('test');
});
describe('.toDate', () => {
test('should work correctly on a date string', () => {
expect(evaluate('={{ "2022-09-01T19:42:28.164Z".toDate() }}')).toEqual(
new Date('2022-09-01T19:42:28.164Z'),
);
});
test('should throw on invalid date', () => {
expect(() => evaluate('={{ "2022-09-32T19:42:28.164Z".toDate() }}')).toThrow(
'cannot convert to date',
);
});
});
test('.toFloat should work correctly on a string', () => {
expect(evaluate('={{ "1.1".toFloat() }}')).toEqual(1.1);
expect(evaluate('={{ "1.1".toDecimalNumber() }}')).toEqual(1.1);
});
test('.toInt should work correctly on a string', () => {
expect(evaluate('={{ "1.1".toInt() }}')).toEqual(1);
expect(evaluate('={{ "1.1".toWholeNumber() }}')).toEqual(1);
expect(evaluate('={{ "1.5".toInt() }}')).toEqual(1);
expect(evaluate('={{ "1.5".toWholeNumber() }}')).toEqual(1);
});
test('.quote should work correctly on a string', () => {
expect(evaluate('={{ "test".quote() }}')).toEqual('"test"');
expect(evaluate('={{ "\\"test\\"".quote() }}')).toEqual('"\\"test\\""');
});
test('.isNumeric should work correctly on a string', () => {
expect(evaluate('={{ "".isNumeric() }}')).toEqual(false);
expect(evaluate('={{ "asdf".isNumeric() }}')).toEqual(false);
expect(evaluate('={{ "1234".isNumeric() }}')).toEqual(true);
expect(evaluate('={{ "4e4".isNumeric() }}')).toEqual(true);
expect(evaluate('={{ "4.4".isNumeric() }}')).toEqual(true);
});
test('.isUrl should work on a string', () => {
expect(evaluate('={{ "https://example.com/".isUrl() }}')).toEqual(true);
expect(evaluate('={{ "http://example.com/".isUrl() }}')).toEqual(true);
expect(evaluate('={{ "ftp://example.com/".isUrl() }}')).toEqual(true);
expect(evaluate('={{ "example.com".isUrl() }}')).toEqual(false);
expect(evaluate('={{ "www.example.com".isUrl() }}')).toEqual(false);
expect(evaluate('={{ "https://www.example.com/".isUrl() }}')).toEqual(true);
expect(evaluate('={{ "https://example.com/path".isUrl() }}')).toEqual(true);
expect(evaluate('={{ "https://example.com/path?query=1".isUrl() }}')).toEqual(true);
expect(evaluate('={{ "https://example.com/path#fragment".isUrl() }}')).toEqual(true);
expect(evaluate('={{ "https://example.com:8080".isUrl() }}')).toEqual(true);
expect(evaluate('={{ "https://example.com?query=1".isUrl() }}')).toEqual(true);
expect(evaluate('={{ "https://example.com#fragment".isUrl() }}')).toEqual(true);
expect(evaluate('={{ "example.com/path".isUrl() }}')).toEqual(false);
expect(evaluate('={{ "http:///".isUrl() }}')).toEqual(false);
expect(evaluate('={{ "https://".isUrl() }}')).toEqual(false);
expect(evaluate('={{ "example".isUrl() }}')).toEqual(false);
expect(evaluate('={{ "".isUrl() }}')).toEqual(false);
});
test('.isDomain should work on a string', () => {
expect(evaluate('={{ "example.com".isDomain() }}')).toEqual(true);
expect(evaluate('={{ "asdf".isDomain() }}')).toEqual(false);
expect(evaluate('={{ "https://example.com/".isDomain() }}')).toEqual(false);
expect(evaluate('={{ "www.example.com".isDomain() }}')).toEqual(true);
expect(evaluate('={{ "subdomain.example.com".isDomain() }}')).toEqual(true);
expect(evaluate('={{ "example.co.uk".isDomain() }}')).toEqual(true);
expect(evaluate('={{ "example".isDomain() }}')).toEqual(false);
expect(evaluate('={{ "example.".isDomain() }}')).toEqual(false);
expect(evaluate('={{ ".com".isDomain() }}')).toEqual(false);
expect(evaluate('={{ "example..com".isDomain() }}')).toEqual(false);
expect(evaluate('={{ "example_com".isDomain() }}')).toEqual(false);
expect(evaluate('={{ "example/com".isDomain() }}')).toEqual(false);
expect(evaluate('={{ "example com".isDomain() }}')).toEqual(false);
expect(evaluate('={{ "www.example..com".isDomain() }}')).toEqual(false);
expect(evaluate('={{ "123.com".isDomain() }}')).toEqual(true);
expect(evaluate('={{ "xn--80aswg.xn--p1ai".isDomain() }}')).toEqual(true); // Punycode domain
expect(evaluate('={{ "example.com:8080".isDomain() }}')).toEqual(true);
expect(evaluate('={{ "".isDomain() }}')).toEqual(false);
});
test('.toSnakeCase should work on a string', () => {
expect(evaluate('={{ "I am a test!".toSnakeCase() }}')).toEqual('i_am_a_test');
expect(evaluate('={{ "i_am_a_test".toSnakeCase() }}')).toEqual('i_am_a_test');
});
test('.toSentenceCase should work on a string', () => {
expect(
evaluate(
'={{ "i am a test! i have multiple types of Punctuation. or do i?".toSentenceCase() }}',
),
).toEqual('I am a test! I have multiple types of punctuation. Or do i?');
expect(evaluate('={{ "i am a test!".toSentenceCase() }}')).toEqual('I am a test!');
expect(evaluate('={{ "i am a test".toSentenceCase() }}')).toEqual('I am a test');
});
test('.extractUrl should work on a string', () => {
expect(
evaluate(
'={{ "I am a test with a url: https://example.net/ and I am a test with an email: test@example.org".extractUrl() }}',
),
).toEqual('https://example.net/');
expect(
evaluate(
'={{ "Check this out: https://subdomain.example.com:3000/path?q=1#hash".extractUrl() }}',
),
).toEqual('https://subdomain.example.com:3000/path?q=1#hash');
expect(evaluate('={{ "Invalid URL: http:///example.com".extractUrl() }}')).toEqual(undefined);
expect(
evaluate(
'={{ "Mixed content: https://www.example.com and http://www.example.org".extractUrl() }}',
),
).toEqual('https://www.example.com');
expect(
evaluate('={{ "Text without URL: This is just a simple text".extractUrl() }}'),
).toEqual(undefined);
expect(
evaluate('={{ "URL with Unicode: http://www.xn--80aswg.xn--j1amh".extractUrl() }}'),
).toEqual('http://www.xn--80aswg.xn--j1amh');
expect(
evaluate('={{ "Localhost URL: http://localhost:8080/test?x=1".extractUrl() }}'),
).toEqual('http://localhost:8080/test?x=1');
expect(
evaluate('={{ "IP URL: http://192.168.1.1:8000/path?q=value#frag".extractUrl() }}'),
).toEqual('http://192.168.1.1:8000/path?q=value#frag');
});
test('.extractDomain should work on a string', () => {
expect(evaluate('={{ "test@example.org".extractDomain() }}')).toEqual('example.org');
expect(evaluate('={{ "https://example.org/".extractDomain() }}')).toEqual('example.org');
expect(evaluate('={{ "https://www.google.com".extractDomain() }}')).toEqual('www.google.com');
expect(evaluate('={{ "http://example.org".extractDomain() }}')).toEqual('example.org');
expect(evaluate('={{ "ftp://ftp.example.com".extractDomain() }}')).toEqual('ftp.example.com');
expect(evaluate('={{ "google.com".extractDomain() }}')).toEqual('google.com');
expect(evaluate('={{ "www.example.net".extractDomain() }}')).toEqual('www.example.net');
expect(evaluate('={{ "//example.com".extractDomain() }}')).toEqual('example.com');
expect(evaluate('={{ "mailto:john.doe@example.com".extractDomain() }}')).toEqual(
'example.com',
);
expect(evaluate('={{ "tel:+1-555-123-4567".extractDomain() }}')).toEqual(undefined);
expect(evaluate('={{ "jane.doe@example.org".extractDomain() }}')).toEqual('example.org');
expect(evaluate('={{ "name+tag@example.com".extractDomain() }}')).toEqual('example.com');
expect(evaluate('={{ "first.last@example.co.uk".extractDomain() }}')).toEqual(
'example.co.uk',
);
expect(evaluate('={{ "user@subdomain.example.com".extractDomain() }}')).toEqual(
'subdomain.example.com',
);
expect(evaluate('={{ "www.example.net?test=1213".extractDomain() }}')).toEqual(
'www.example.net',
);
expect(evaluate('={{ "www.example.net?test".extractDomain() }}')).toEqual('www.example.net');
expect(evaluate('={{ "www.example.net#tesdt123".extractDomain() }}')).toEqual(
'www.example.net',
);
expect(evaluate('={{ "https://www.example.net?test=1213".extractDomain() }}')).toEqual(
'www.example.net',
);
expect(evaluate('={{ "https://www.example.net?test".extractDomain() }}')).toEqual(
'www.example.net',
);
expect(evaluate('={{ "https://www.example.net#tesdt123".extractDomain() }}')).toEqual(
'www.example.net',
);
expect(evaluate('={{ "https://192.168.1.1".extractDomain() }}')).toEqual('192.168.1.1');
expect(evaluate('={{ "http://www.xn--80aswg.xn--j1amh".extractDomain() }}')).toEqual(
'www.xn--80aswg.xn--j1amh',
);
expect(evaluate('={{ "https://localhost".extractDomain() }}')).toEqual('localhost');
expect(evaluate('={{ "https://localhost?test=123".extractDomain() }}')).toEqual('localhost');
expect(evaluate('={{ "https://www.example_with_underscore.com".extractDomain() }}')).toEqual(
'www.example_with_underscore.com',
);
expect(evaluate('={{ "https://www.example.com:8080".extractDomain() }}')).toEqual(
'www.example.com',
);
expect(evaluate('={{ "https://example.space".extractDomain() }}')).toEqual('example.space');
});
test('.extractEmail should work on a string', () => {
expect(
evaluate(
'={{ "I am a test with a url: https://example.net/ and I am a test with an email: test@example.org".extractEmail() }}',
),
).toEqual('test@example.org');
});
test('.isEmail should work on a string', () => {
expect(evaluate('={{ "test@example.com".isEmail() }}')).toEqual(true);
expect(evaluate('={{ "aaaaaaaa".isEmail() }}')).toEqual(false);
expect(evaluate('={{ "test @ n8n".isEmail() }}')).toEqual(false);
});
test('.toDateTime should work on a variety of formats', () => {
expect(evaluate('={{ "Wed, 21 Oct 2015 07:28:00 GMT".toDateTime() }}')).toBeInstanceOf(
DateTime,
);
expect(evaluate('={{ "2008-11-11".toDateTime() }}')).toBeInstanceOf(DateTime);
expect(evaluate('={{ "1-Feb-2024".toDateTime() }}')).toBeInstanceOf(DateTime);
expect(evaluate('={{ "1713976144063".toDateTime("ms") }}')).toBeInstanceOf(DateTime);
expect(evaluate('={{ "31-01-2024".toDateTime("dd-MM-yyyy") }}')).toBeInstanceOf(DateTime);
vi.useFakeTimers({ now: new Date() });
expect(() => evaluate('={{ "hi".toDateTime() }}')).toThrow(
new ExpressionExtensionError('cannot convert to Luxon DateTime'),
);
vi.useRealTimers();
});
test('.extractUrlPath should work on a string', () => {
expect(
evaluate('={{ "https://example.com/orders/1/detail#hash?foo=bar".extractUrlPath() }}'),
).toEqual('/orders/1/detail');
expect(evaluate('={{ "hi".extractUrlPath() }}')).toBeUndefined();
});
test('.parseJson should work on a string', () => {
expect(evaluate('={{ \'{"test1":1,"test2":"2"}\'.parseJson() }}')).toEqual({
test1: 1,
test2: '2',
});
});
test('.parseJson should throw on invalid JSON', () => {
expect(() => evaluate("={{ \"{'test1':1,'test2':'2'}\".parseJson() }}")).toThrowError(
"Parsing failed. Check you're using double quotes",
);
expect(() => evaluate('={{ "No JSON here".parseJson() }}')).toThrowError('Parsing failed');
});
test('.toJsonString should work on a string', () => {
expect(evaluate('={{ "test".toJsonString() }}')).toEqual(JSON.stringify('test'));
expect(evaluate('={{ "The \\"best\\" colours: red\\nbrown".toJsonString() }}')).toEqual(
JSON.stringify('The "best" colours: red\nbrown'),
);
expect(evaluate('={{ "".toJsonString() }}')).toEqual(JSON.stringify(''));
});
test('.toBoolean should work on a string', () => {
expect(evaluate('={{ "False".toBoolean() }}')).toBe(false);
expect(evaluate('={{ "".toBoolean() }}')).toBe(false);
expect(evaluate('={{ "0".toBoolean() }}')).toBe(false);
expect(evaluate('={{ "no".toBoolean() }}')).toBe(false);
expect(evaluate('={{ "TRUE".toBoolean() }}')).toBe(true);
expect(evaluate('={{ "hello".toBoolean() }}')).toBe(true);
});
test('.base64Encode should work on a string', () => {
expect(evaluate('={{ "n8n test".base64Encode() }}')).toBe('bjhuIHRlc3Q=');
});
test('.base64Decode should work on a string', () => {
expect(evaluate('={{ "bjhuIHRlc3Q=".base64Decode() }}')).toBe('n8n test');
});
});
});