mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat: Expression extension framework (#4372)
* ⚡ Introduce a framework for expression extension * 💡 Add some inline comments * ⚡ Introduce hash alias for encrypt * ⚡ Introduce a manual granular level approach to shadowing/overrideing extensions * 🔥 Cleanup comments * ⚡ Introduce a basic method of extension for native functions * ⚡ Add length to StringExtension * ⚡ Add number type to extension return types * ⚡ Temporarily introduce DateTime with extension * ⚡ Cleanup comments * ⚡ Organize imports * ♻️ Fix up some typings * ⚡ Fix typings * ♻️ Remove unnecessary resolve of expression * ⚡ Extensions Improvement * ♻️ Refactor EXPRESSION_EXTENSION_METHODS * ♻️ Refactor EXPRESSION_EXTENSION_METHODS * ♻️ Update extraArgs types * ♻️ Fix tests * ♻️ Fix bind type issue * ♻️ Fixing duration type issue * ♻️ Refactor to allow overrides on native methods * ♻️ Temporarily remove Date Extensions to pass tests * feat(dt-functions): introduce date expression extensions (#4045) * 🎉 Add Date Extensions into the mix * ✨ Introduce additional date extension methods * ✅ Add Date Expression Extension tests * 🔧 Add ability to debug tests * ♻️ Refactor extension for native types * 🔥 Move sayHi method to String Extension class * ♻️ Update scope when binding member methods * ✅ Add String Extension tests * feat(dt-functions): introduce array expression extensions (#4044) * ✨ Introduce Array Extensions * ✅ Add Array Expression tests * feat(dt-functions): introduce number expression extensions (#4046) * 🎉 Introduce Number Extensions * ⚡ Support more shared extensions * ⚡ Improve handling of name collision * ✅ Update tests * Fixed up tests * 🔥 Remove remove markdown * :recylce: Replace remove-markdown dependencies with implementation * ♻️ Replace remove-markdown dependencies with implementation * ✅ Update tests * ♻️ Fix scoping and cleanup * ♻️ Update comments and errors * ♻️ Fix linting errors * ➖ Remove unused dependencies * fix: expression extension not working with multiple extensions * refactor: change extension transform to be more efficient * test: update most test to work with new extend function * fix: update and fix type error in config * refactor: replace babel with recast * feat: add hashing functions to string extension * fix: removed export * test: add extension parser and transform tests * fix: vite tests breaking * refactor: remove commented out code * fix: parse dates passed from $json in extend function * refactor: review feedback changes for date extensions * refactor: review feedback changes for number extensions * fix: date extension beginningOf test * fix: broken build from merge * fix: another merge issue * refactor: address review feedback (remove ignores) * feat: new extension functions and tests * feat: non-dot notation functions * test: most of the other tests * fix: toSentenceCase for node versions below 16.6 * feat: add $if and $not expression extensions * Fix test to work on every timezone * lint: fix remaining lint issues Co-authored-by: Csaba Tuncsik <csaba@n8n.io> Co-authored-by: Omar Ajoue <krynble@gmail.com>
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { evaluate } from './Helpers';
|
||||
|
||||
describe('Data Transformation Functions', () => {
|
||||
describe('Array Data Transformation Functions', () => {
|
||||
test('.random() should work correctly on an array', () => {
|
||||
expect(evaluate('={{ [1,2,3].random() }}')).not.toBeUndefined();
|
||||
});
|
||||
|
||||
test('.randomItem() alias should work correctly on an array', () => {
|
||||
expect(evaluate('={{ [1,2,3].randomItem() }}')).not.toBeUndefined();
|
||||
});
|
||||
|
||||
test('.isPresent() should work correctly on an array', () => {
|
||||
expect(evaluate('={{ [1,2,3, "imhere"].isPresent() }}')).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' }
|
||||
].pluck("value") }}`),
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ value: 1 },
|
||||
{ value: 2 },
|
||||
{ value: 3 },
|
||||
{ value: 4 },
|
||||
{ value: 5 },
|
||||
{ value: 6 },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
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('.isBlank() should work correctly on an array', () => {
|
||||
expect(evaluate('={{ [].isBlank() }}')).toEqual(true);
|
||||
});
|
||||
|
||||
test('.isBlank() should work correctly on an array', () => {
|
||||
expect(evaluate('={{ [1].isBlank() }}')).toEqual(false);
|
||||
});
|
||||
|
||||
test('.length() should work correctly on an array', () => {
|
||||
expect(evaluate('={{ [].length() }}')).toEqual(0);
|
||||
});
|
||||
|
||||
test('.count() should work correctly on an array', () => {
|
||||
expect(evaluate('={{ [1].count() }}')).toEqual(1);
|
||||
});
|
||||
|
||||
test('.size() should work correctly on an array', () => {
|
||||
expect(evaluate('={{ [1,2].size() }}')).toEqual(2);
|
||||
});
|
||||
|
||||
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('.filter() should work correctly on an array', () => {
|
||||
expect(evaluate('={{ ["repeat","repeat","a","b","c"].filter("repeat") }}')).toEqual(
|
||||
expect.arrayContaining(['repeat', '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 },
|
||||
{ test1: 1, test3: 3, test4: 4 },
|
||||
]);
|
||||
});
|
||||
|
||||
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, 6].sum() }}')).toEqual(21);
|
||||
expect(evaluate('={{ ["1", 2, 3, 4, 5, "bad"].sum() }}')).toBeNaN();
|
||||
});
|
||||
|
||||
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, 6].average() }}')).toEqual(3.5);
|
||||
expect(evaluate('={{ ["1", 2, 3, 4, 5, "bad"].average() }}')).toBeNaN();
|
||||
});
|
||||
|
||||
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, 6].min() }}')).toEqual(1);
|
||||
expect(evaluate('={{ ["1", 2, 3, 4, 5, "bad"].min() }}')).toBeNaN();
|
||||
});
|
||||
|
||||
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, 6].max() }}')).toEqual(6);
|
||||
expect(evaluate('={{ ["1", 2, 3, 4, 5, "bad"].max() }}')).toBeNaN();
|
||||
});
|
||||
|
||||
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('.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('.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('.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('.filter() should work on a list of strings', () => {
|
||||
expect(
|
||||
evaluate(
|
||||
'={{ ["i am a test string", "i should be kept", "i should be removed test"].filter("test", "remove") }}',
|
||||
),
|
||||
).toEqual(['i should be kept']);
|
||||
expect(
|
||||
evaluate(
|
||||
'={{ ["i am a test string", "i should be kept test", "i should be removed"].filter("test") }}',
|
||||
),
|
||||
).toEqual(['i am a test string', 'i should be kept test']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { extend } from '@/Extensions';
|
||||
import { dateExtensions } from '@/Extensions/DateExtensions';
|
||||
import { evaluate } from './Helpers';
|
||||
|
||||
describe('Data Transformation Functions', () => {
|
||||
describe('Date Data Transformation Functions', () => {
|
||||
test('.isWeekend() should work correctly on a date', () => {
|
||||
expect(evaluate('={{DateTime.now().isWeekend()}}')).toEqual(
|
||||
extend(new Date(), 'isWeekend', []),
|
||||
);
|
||||
});
|
||||
|
||||
test('.toTimeFromNow() should work correctly on a date', () => {
|
||||
const JUST_NOW_STRING_RESULT = 'just now';
|
||||
expect(evaluate('={{DateTime.now().toTimeFromNow()}}')).toEqual(JUST_NOW_STRING_RESULT);
|
||||
});
|
||||
|
||||
test('.beginningOf("week") should work correctly on a date', () => {
|
||||
expect(evaluate('={{(new Date).beginningOf("week")}}')).toEqual(
|
||||
dateExtensions.functions.beginningOf(new Date(), ['week']),
|
||||
);
|
||||
});
|
||||
|
||||
test('.endOfMonth() should work correctly on a date', () => {
|
||||
expect(evaluate('={{ DateTime.now().endOfMonth() }}')).toEqual(
|
||||
dateExtensions.functions.endOfMonth(new Date()),
|
||||
);
|
||||
});
|
||||
|
||||
test('.extract("day") should work correctly on a date', () => {
|
||||
expect(evaluate('={{ DateTime.now().extract("day") }}')).toEqual(
|
||||
dateExtensions.functions.extract(new Date(), ['day']),
|
||||
);
|
||||
});
|
||||
|
||||
test('.format("yyyy LLL dd") should work correctly on a date', () => {
|
||||
expect(evaluate('={{ DateTime.now().format("yyyy LLL dd") }}')).toEqual(
|
||||
dateExtensions.functions.format(new Date(), ['yyyy LLL dd']),
|
||||
);
|
||||
expect(evaluate('={{ DateTime.now().format("yyyy LLL dd") }}')).not.toEqual(
|
||||
dateExtensions.functions.format(new Date(), ["HH 'hours and' mm 'minutes'"]),
|
||||
);
|
||||
});
|
||||
|
||||
test('.toDate() should work on a string', () => {
|
||||
expect(evaluate('={{ "2022-01-03T00:00:00.000+00:00".toDate() }}')).toEqual(new Date(2022, 0, 3));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { extendTransform } from '@/Extensions';
|
||||
import { joinExpression, splitExpression } from '@/Extensions/ExpressionParser';
|
||||
import { evaluate } from './Helpers';
|
||||
|
||||
describe('Expression Extension Transforms', () => {
|
||||
describe('extend() transform', () => {
|
||||
test('Basic transform with .isBlank', () => {
|
||||
expect(extendTransform('"".isBlank()')!.code).toEqual('extend("", "isBlank", [])');
|
||||
});
|
||||
|
||||
test('Chained transform with .sayHi.getOnlyFirstCharacters', () => {
|
||||
expect(extendTransform('"".sayHi().getOnlyFirstCharacters(2)')!.code).toEqual(
|
||||
'extend(extend("", "sayHi", []), "getOnlyFirstCharacters", [2])',
|
||||
);
|
||||
});
|
||||
|
||||
test('Chained transform with native functions .sayHi.trim.getOnlyFirstCharacters', () => {
|
||||
expect(extendTransform('"aaa ".sayHi().trim().getOnlyFirstCharacters(2)')!.code).toEqual(
|
||||
'extend(extend("aaa ", "sayHi", []).trim(), "getOnlyFirstCharacters", [2])',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('tmpl 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".sayHi() }} you have ${{ (100).format() }}.')).toEqual([
|
||||
{ type: 'text', text: '' },
|
||||
{ type: 'code', text: ' "test".sayHi() ', hasClosingBrackets: true },
|
||||
{ type: 'text', text: ' you have $' },
|
||||
{ type: 'code', text: ' (100).format() ', hasClosingBrackets: true },
|
||||
{ type: 'text', text: '.' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('Unclosed expression', () => {
|
||||
expect(splitExpression('{{ "test".sayHi() }} you have ${{ (100).format()')).toEqual([
|
||||
{ type: 'text', text: '' },
|
||||
{ type: 'code', text: ' "test".sayHi() ', 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".sayHi() }} you have ${{ (100).format() }}.')),
|
||||
).toEqual('{{ "test".sayHi() }} you have ${{ (100).format() }}.');
|
||||
});
|
||||
|
||||
test('Unclosed expression', () => {
|
||||
expect(
|
||||
joinExpression(splitExpression('{{ "test".sayHi() }} you have ${{ (100).format()')),
|
||||
).toEqual('{{ "test".sayHi() }} you have ${{ (100).format()');
|
||||
});
|
||||
|
||||
test('Escaped opening bracket', () => {
|
||||
expect(joinExpression(splitExpression('test \\{{ no code }}'))).toEqual(
|
||||
'test \\{{ no code }}',
|
||||
);
|
||||
});
|
||||
|
||||
test('Escaped closinging bracket', () => {
|
||||
expect(joinExpression(splitExpression('test {{ code.test("\\}}") }}'))).toEqual(
|
||||
'test {{ code.test("\\}}") }}',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
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 = jest.fn(() => false);
|
||||
// @ts-ignore
|
||||
evaluate('={{ $if("a"==="a", true, $data["cb"]()) }}', [{ cb: mockCallback }]);
|
||||
expect(mockCallback.mock.calls.length).toEqual(0);
|
||||
|
||||
// @ts-ignore
|
||||
evaluate('={{ $if("a"==="b", true, $data["cb"]()) }}', [{ cb: mockCallback }]);
|
||||
expect(mockCallback.mock.calls.length).toEqual(0);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { evaluate } from './Helpers';
|
||||
|
||||
describe('Data Transformation Functions', () => {
|
||||
describe('Genric Data Transformation Functions', () => {
|
||||
test('.isBlank() should work correctly on undefined', () => {
|
||||
expect(evaluate('={{(undefined).isBlank()}}')).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
33
packages/workflow/test/ExpressionExtensions/Helpers.ts
Normal file
33
packages/workflow/test/ExpressionExtensions/Helpers.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Expression, INodeExecutionData, Workflow } from '../../src';
|
||||
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 = new Expression(workflow);
|
||||
|
||||
export const evaluate = (value: string, values?: INodeExecutionData[]) =>
|
||||
expression.getParameterValue(
|
||||
value,
|
||||
null,
|
||||
0,
|
||||
0,
|
||||
'node',
|
||||
values ?? [],
|
||||
'manual',
|
||||
'America/New_York',
|
||||
{},
|
||||
);
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { numberExtensions } from '@/Extensions/NumberExtensions';
|
||||
import { evaluate } from './Helpers';
|
||||
|
||||
describe('Data Transformation Functions', () => {
|
||||
describe('Number Data Transformation Functions', () => {
|
||||
test('.random() should work correctly on a number', () => {
|
||||
expect(evaluate('={{ Number(100).random() }}')).not.toBeUndefined();
|
||||
});
|
||||
|
||||
test('.isBlank() should work correctly on a number', () => {
|
||||
expect(evaluate('={{ Number(100).isBlank() }}')).toEqual(false);
|
||||
});
|
||||
|
||||
test('.isPresent() should work correctly on a number', () => {
|
||||
expect(evaluate('={{ Number(100).isPresent() }}')).toEqual(
|
||||
numberExtensions.functions.isPresent(100),
|
||||
);
|
||||
});
|
||||
|
||||
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('.isTrue() should work on a number', () => {
|
||||
expect(evaluate('={{ (1).isTrue() }}')).toEqual(true);
|
||||
expect(evaluate('={{ (0).isTrue() }}')).toEqual(false);
|
||||
expect(evaluate('={{ (NaN).isTrue() }}')).toEqual(false);
|
||||
});
|
||||
|
||||
test('.isFalse() should work on a number', () => {
|
||||
expect(evaluate('={{ (1).isFalse() }}')).toEqual(false);
|
||||
expect(evaluate('={{ (0).isFalse() }}')).toEqual(true);
|
||||
expect(evaluate('={{ (NaN).isFalse() }}')).toEqual(false);
|
||||
});
|
||||
|
||||
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);
|
||||
expect(evaluate('={{ (NaN).isOdd() }}')).toEqual(false);
|
||||
});
|
||||
|
||||
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);
|
||||
expect(evaluate('={{ (NaN).isEven() }}')).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multiple expressions', () => {
|
||||
test('Basic multiple expressions', () => {
|
||||
expect(evaluate('={{ "Test".sayHi() }} you have ${{ (100).format() }}.')).toEqual(
|
||||
'hi Test you have $100.',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import { evaluate } from './Helpers';
|
||||
|
||||
describe('Data Transformation Functions', () => {
|
||||
describe('Object Data Transformation Functions', () => {
|
||||
test('.isEmpty() should work correctly on an object', () => {
|
||||
expect(evaluate('={{({}).isEmpty()}}')).toEqual(true);
|
||||
expect(evaluate('={{({ test1: 1 }).isEmpty()}}')).toEqual(false);
|
||||
});
|
||||
|
||||
test('.merge should work on an object', () => {
|
||||
expect(evaluate('={{ ({ test1: 1, test2: 2 }).merge({ test2: 3, test3: 3 }) }}')).toEqual({
|
||||
test1: 1,
|
||||
test2: 2,
|
||||
test3: 3,
|
||||
});
|
||||
});
|
||||
|
||||
test('.hasField should work on an object', () => {
|
||||
expect(evaluate('={{ ({ test1: 1 }).hasField("test1") }}')).toEqual(true);
|
||||
expect(evaluate('={{ ({ test1: 1 }).hasField("test2") }}')).toEqual(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,
|
||||
});
|
||||
});
|
||||
|
||||
test('.removeFieldsContaining 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('.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('.compact should work on an object', () => {
|
||||
expect(
|
||||
evaluate('={{ ({ test1: 1, test2: "2", test3: undefined, test4: null }).compact() }}'),
|
||||
).toEqual({ test1: 1, test2: '2' });
|
||||
});
|
||||
|
||||
test('.urlEncode should work on an object', () => {
|
||||
expect(evaluate('={{ ({ test1: 1, test2: "2" }).urlEncode() }}')).toEqual('test1=1&test2=2');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { stringExtensions } from '@/Extensions/StringExtensions';
|
||||
import { dateExtensions } from '@/Extensions/DateExtensions';
|
||||
import { evaluate } from './Helpers';
|
||||
|
||||
describe('Data Transformation Functions', () => {
|
||||
describe('String Data Transformation Functions', () => {
|
||||
test('.isBlank() should work correctly on a string that is not empty', () => {
|
||||
expect(evaluate('={{"NotBlank".isBlank()}}')).toEqual(false);
|
||||
});
|
||||
|
||||
test('.isBlank() should work correctly on a string that is empty', () => {
|
||||
expect(evaluate('={{"".isBlank()}}')).toEqual(true);
|
||||
});
|
||||
|
||||
test('.getOnlyFirstCharacters() should work correctly on a string', () => {
|
||||
expect(evaluate('={{"myNewField".getOnlyFirstCharacters(5)}}')).toEqual('myNew');
|
||||
|
||||
expect(evaluate('={{"myNewField".getOnlyFirstCharacters(10)}}')).toEqual('myNewField');
|
||||
|
||||
expect(
|
||||
evaluate('={{"myNewField".getOnlyFirstCharacters(5).length >= "myNewField".length}}'),
|
||||
).toEqual(false);
|
||||
|
||||
expect(evaluate('={{DateTime.now().toLocaleString().getOnlyFirstCharacters(2)}}')).toEqual(
|
||||
stringExtensions.functions.getOnlyFirstCharacters(
|
||||
// @ts-ignore
|
||||
dateExtensions.functions.toLocaleString(new Date(), []),
|
||||
[2],
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('.sayHi() should work correctly on a string', () => {
|
||||
expect(evaluate('={{ "abc".sayHi() }}')).toEqual('hi abc');
|
||||
});
|
||||
|
||||
test('.encrypt() should work correctly on a string', () => {
|
||||
expect(evaluate('={{ "12345".encrypt("sha256") }}')).toEqual(
|
||||
stringExtensions.functions.encrypt('12345', ['sha256']),
|
||||
);
|
||||
|
||||
expect(evaluate('={{ "12345".encrypt("sha256") }}')).not.toEqual(
|
||||
stringExtensions.functions.encrypt('12345', ['MD5']),
|
||||
);
|
||||
|
||||
expect(evaluate('={{ "12345".encrypt("MD5") }}')).toEqual(
|
||||
stringExtensions.functions.encrypt('12345', ['MD5']),
|
||||
);
|
||||
|
||||
expect(evaluate('={{ "12345".hash("sha256") }}')).toEqual(
|
||||
'5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5',
|
||||
);
|
||||
});
|
||||
|
||||
test('.hash() alias should work correctly on a string', () => {
|
||||
expect(evaluate('={{ "12345".hash("sha256") }}')).toEqual(
|
||||
'5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5',
|
||||
);
|
||||
});
|
||||
|
||||
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('.stripTags should work correctly on a string', () => {
|
||||
expect(evaluate('={{ "<html><head>test</head></html>".stripTags() }}')).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');
|
||||
});
|
||||
|
||||
test('.toDate 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('.toBoolean should work correctly on a string', () => {
|
||||
const validTrue = ['y', 'yes', 't', 'true', '1', 'YES'];
|
||||
for (const v of validTrue) {
|
||||
expect(evaluate(`={{ "${v}".toBoolean() }}`)).toEqual(true);
|
||||
}
|
||||
|
||||
const validFalse = ['n', 'no', 'f', 'false', '0', 'NO'];
|
||||
for (const v of validFalse) {
|
||||
expect(evaluate(`={{ "${v}".toBoolean() }}`)).toEqual(false);
|
||||
}
|
||||
|
||||
expect(evaluate('={{ "maybe".toBoolean() }}')).toEqual(false);
|
||||
});
|
||||
|
||||
test('.isTrue should work correctly on a string', () => {
|
||||
const validTrue = ['y', 'yes', 't', 'true', '1', 'YES'];
|
||||
for (const v of validTrue) {
|
||||
expect(evaluate(`={{ "${v}".isTrue() }}`)).toEqual(true);
|
||||
}
|
||||
|
||||
const validFalse = ['n', 'no', 'f', 'false', '0', 'NO'];
|
||||
for (const v of validFalse) {
|
||||
expect(evaluate(`={{ "${v}".isTrue() }}`)).toEqual(false);
|
||||
}
|
||||
|
||||
expect(evaluate('={{ "maybe".isTrue() }}')).toEqual(false);
|
||||
});
|
||||
|
||||
test('.isFalse should work correctly on a string', () => {
|
||||
const validTrue = ['y', 'yes', 't', 'true', '1', 'YES'];
|
||||
for (const v of validTrue) {
|
||||
expect(evaluate(`={{ "${v}".isFalse() }}`)).toEqual(false);
|
||||
}
|
||||
|
||||
const validFalse = ['n', 'no', 'f', 'false', '0', 'NO'];
|
||||
for (const v of validFalse) {
|
||||
expect(evaluate(`={{ "${v}".isFalse() }}`)).toEqual(true);
|
||||
}
|
||||
|
||||
expect(evaluate('={{ "maybe".isFalse() }}')).toEqual(false);
|
||||
});
|
||||
|
||||
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('={{ "example.com".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);
|
||||
});
|
||||
|
||||
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('.toTitleCase should work on a string', () => {
|
||||
expect(
|
||||
evaluate(
|
||||
'={{ "i am a test! i have multiple types of Punctuation. or do i?".toTitleCase() }}',
|
||||
),
|
||||
).toEqual('I Am A Test! I Have Multiple Types Of Punctuation. Or Do I?');
|
||||
expect(evaluate('={{ "i am a test!".toTitleCase() }}')).toEqual('I Am A Test!');
|
||||
expect(evaluate('={{ "i am a test".toTitleCase() }}')).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/');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user