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:
Valya
2023-01-10 13:06:12 +00:00
committed by GitHub
parent 871a1d7dad
commit 3d05acf313
25 changed files with 2529 additions and 5 deletions

View File

@@ -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']);
});
});
});

View File

@@ -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));
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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);
});
});
});

View 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',
{},
);

View File

@@ -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.',
);
});
});
});

View File

@@ -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');
});
});
});

View File

@@ -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);
});
});
});