feat(editor): Add examples for object and array expression methods (#9360)

Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
This commit is contained in:
Elias Meire 2024-05-14 16:32:31 +02:00 committed by GitHub
parent 78e7c7a9da
commit 52936633af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 890 additions and 193 deletions

View file

@ -466,9 +466,20 @@ const stringOptions = (input: AutocompleteInput<string>): Completion[] => {
]);
if (resolved && validateFieldType('string', resolved, 'number').valid) {
const recommended = ['toNumber()'];
const timestampUnit = toTimestampUnit(Number(resolved));
if (timestampUnit) {
return applySections({
options,
recommended: ['toNumber()'],
recommended: [...recommended, { label: 'toDateTime()', args: [`'${timestampUnit}'`] }],
sections: STRING_SECTIONS,
});
}
return applySections({
options,
recommended,
sections: STRING_SECTIONS,
});
}
@ -541,6 +552,29 @@ const booleanOptions = (): Completion[] => {
});
};
const isWithinMargin = (ts: number, now: number, margin: number): boolean => {
return ts > now - margin && ts < now + margin;
};
const toTimestampUnit = (ts: number): null | 'ms' | 's' | 'us' => {
const nowMillis = Date.now();
const marginMillis = 946_707_779_000; // 30y
if (isWithinMargin(ts, nowMillis, marginMillis)) {
return 'ms';
}
if (isWithinMargin(ts, nowMillis / 1000, marginMillis / 1000)) {
return 's';
}
if (isWithinMargin(ts, nowMillis * 1000, marginMillis * 1000)) {
return 'us';
}
return null;
};
const numberOptions = (input: AutocompleteInput<number>): Completion[] => {
const { resolved, transformLabel } = input;
const options = sortCompletionsAlpha([
@ -550,26 +584,11 @@ const numberOptions = (input: AutocompleteInput<number>): Completion[] => {
const ONLY_INTEGER = ['isEven()', 'isOdd()'];
if (Number.isInteger(resolved)) {
const nowMillis = Date.now();
const marginMillis = 946_707_779_000; // 30y
const isPlausableMillisDateTime =
resolved > nowMillis - marginMillis && resolved < nowMillis + marginMillis;
if (isPlausableMillisDateTime) {
const timestampUnit = toTimestampUnit(resolved);
if (timestampUnit) {
return applySections({
options,
recommended: [{ label: 'toDateTime()', args: ["'ms'"] }],
});
}
const nowSeconds = nowMillis / 1000;
const marginSeconds = marginMillis / 1000;
const isPlausableSecondsDateTime =
resolved > nowSeconds - marginSeconds && resolved < nowSeconds + marginSeconds;
if (isPlausableSecondsDateTime) {
return applySections({
options,
recommended: [{ label: 'toDateTime()', args: ["'s'"] }],
recommended: [{ label: 'toDateTime()', args: [`'${timestampUnit}'`] }],
});
}

View file

@ -110,19 +110,7 @@ const renderDescription = ({
return descriptionBody;
};
const renderArgs = (args: DocMetadataArgument[]) => {
const argsContainer = document.createElement('div');
argsContainer.classList.add('autocomplete-info-args-container');
const argsTitle = document.createElement('div');
argsTitle.classList.add('autocomplete-info-section-title');
argsTitle.textContent = i18n.baseText('codeNodeEditor.parameters');
argsContainer.appendChild(argsTitle);
const argsList = document.createElement('ul');
argsList.classList.add('autocomplete-info-args');
for (const arg of args.filter((a) => a.name !== '...')) {
const renderArg = (arg: DocMetadataArgument) => {
const argItem = document.createElement('li');
const argName = document.createElement('span');
argName.classList.add('autocomplete-info-arg-name');
@ -133,11 +121,11 @@ const renderArgs = (args: DocMetadataArgument[]) => {
tags.push(arg.type);
}
if (arg.optional || arg.name.endsWith('?')) {
if (!!arg.optional || arg.name.endsWith('?')) {
tags.push(i18n.baseText('codeNodeEditor.optional'));
}
if (args.length > 0) {
if (tags.length > 0) {
argName.textContent += ` (${tags.join(', ')})`;
}
@ -150,7 +138,7 @@ const renderArgs = (args: DocMetadataArgument[]) => {
const argDescription = document.createElement('span');
argDescription.classList.add('autocomplete-info-arg-description');
if (arg.default && !arg.description.toLowerCase().includes('default')) {
if (arg.default && arg.optional && !arg.description.toLowerCase().includes('default')) {
const separator = arg.description.endsWith('.') ? ' ' : '. ';
arg.description +=
separator +
@ -159,17 +147,38 @@ const renderArgs = (args: DocMetadataArgument[]) => {
});
}
argDescription.innerHTML = sanitizeHtml(
arg.description.replace(/`(.*?)`/g, '<code>$1</code>'),
);
argDescription.innerHTML = sanitizeHtml(arg.description.replace(/`(.*?)`/g, '<code>$1</code>'));
argItem.appendChild(argDescription);
}
argsList.appendChild(argItem);
if (Array.isArray(arg.args)) {
argItem.appendChild(renderArgList(arg.args));
}
argsContainer.appendChild(argsList);
return argItem;
};
const renderArgList = (args: DocMetadataArgument[]) => {
const argsList = document.createElement('ul');
argsList.classList.add('autocomplete-info-args');
for (const arg of args) {
argsList.appendChild(renderArg(arg));
}
return argsList;
};
const renderArgs = (args: DocMetadataArgument[]) => {
const argsContainer = document.createElement('div');
argsContainer.classList.add('autocomplete-info-args-container');
const argsTitle = document.createElement('div');
argsTitle.classList.add('autocomplete-info-section-title');
argsTitle.textContent = i18n.baseText('codeNodeEditor.parameters');
argsContainer.appendChild(argsTitle);
argsContainer.appendChild(renderArgList(args));
return argsContainer;
};

View file

@ -197,6 +197,11 @@
li + li {
margin-top: var(--spacing-4xs);
}
.autocomplete-info-args {
margin-top: var(--spacing-4xs);
padding-left: var(--spacing-s);
}
}
.autocomplete-info-arg-name {

View file

@ -3,6 +3,7 @@ import { ExpressionExtensionError } from '../errors/expression-extension.error';
import type { Extension, ExtensionMap } from './Extensions';
import { compact as oCompact } from './ObjectExtensions';
import deepEqual from 'deep-equal';
import uniqWith from 'lodash/uniqWith';
function first(value: unknown[]): unknown {
return value[0];
@ -52,31 +53,18 @@ function randomItem(value: unknown[]): unknown {
}
function unique(value: unknown[], extraArgs: string[]): unknown[] {
if (extraArgs.length) {
return value.reduce<unknown[]>((l, v) => {
if (typeof v === 'object' && v !== null && extraArgs.every((i) => i in v)) {
const alreadySeen = l.find((i) =>
extraArgs.every((j) =>
deepEqual(
(i as Record<string, unknown>)[j],
(v as Record<string, unknown>, { strict: true })[j],
{ strict: true },
),
),
const mapForEqualityCheck = (item: unknown): unknown => {
if (extraArgs.length > 0 && item && typeof item === 'object') {
return extraArgs.reduce<Record<string, unknown>>((acc, key) => {
acc[key] = (item as Record<string, unknown>)[key];
return acc;
}, {});
}
return item;
};
return uniqWith(value, (a, b) =>
deepEqual(mapForEqualityCheck(a), mapForEqualityCheck(b), { strict: true }),
);
if (!alreadySeen) {
l.push(v);
}
}
return l;
}, []);
}
return value.reduce<unknown[]>((l, v) => {
if (l.findIndex((i) => deepEqual(i, v, { strict: true })) === -1) {
l.push(v);
}
return l;
}, []);
}
const ensureNumberArray = (arr: unknown[], { fnName }: { fnName: string }) => {
@ -320,6 +308,10 @@ function intersection(value: unknown[], extraArgs: unknown[][]): unknown[] {
return unique(newArr, []);
}
function append(value: unknown[], extraArgs: unknown[][]): unknown[] {
return value.concat(extraArgs);
}
export function toJsonString(value: unknown[]) {
return JSON.stringify(value);
}
@ -342,97 +334,145 @@ export function toDateTime() {
average.doc = {
name: 'average',
description: 'Returns the mean average of all values in the array.',
description:
'Returns the average of the numbers in the array. Throws an error if there are any non-numbers.',
examples: [{ example: '[12, 1, 5].average()', evaluated: '6' }],
returnType: 'number',
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/arrays/#array-average',
};
compact.doc = {
name: 'compact',
description: 'Removes all empty values from the array.',
description:
'Removes any empty values from the array. <code>null</code>, <code>""</code> and <code>undefined</code> count as empty.',
examples: [{ example: '[2, null, 1, ""].compact()', evaluated: '[2, 1]' }],
returnType: 'Array',
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/arrays/#array-compact',
};
isEmpty.doc = {
name: 'isEmpty',
description: 'Checks if the array doesnt have any elements.',
description: 'Returns <code>true</code> if the array has no elements',
examples: [
{ example: '[].isEmpty()', evaluated: 'true' },
{ example: "['quick', 'brown', 'fox'].isEmpty()", evaluated: 'false' },
],
returnType: 'boolean',
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/arrays/#array-isEmpty',
};
isNotEmpty.doc = {
name: 'isNotEmpty',
description: 'Checks if the array has elements.',
description: 'Returns <code>true</code> if the array has at least one element',
examples: [
{ example: "['quick', 'brown', 'fox'].isNotEmpty()", evaluated: 'true' },
{ example: '[].isNotEmpty()', evaluated: 'false' },
],
returnType: 'boolean',
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/arrays/#array-isNotEmpty',
};
first.doc = {
name: 'first',
description: 'Returns the first element of the array.',
returnType: 'Element',
description: 'Returns the first element of the array',
examples: [{ example: "['quick', 'brown', 'fox'].first()", evaluated: "'quick'" }],
returnType: 'any',
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/arrays/#array-first',
};
last.doc = {
name: 'last',
description: 'Returns the last element of the array.',
returnType: 'Element',
description: 'Returns the last element of the array',
examples: [{ example: "['quick', 'brown', 'fox'].last()", evaluated: "'fox'" }],
returnType: 'any',
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/arrays/#array-last',
};
max.doc = {
name: 'max',
description: 'Gets the maximum value from a number-only array.',
description:
'Returns the largest number in the array. Throws an error if there are any non-numbers.',
examples: [{ example: '[1, 12, 5].max()', evaluated: '12' }],
returnType: 'number',
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/arrays/#array-max',
};
min.doc = {
name: 'min',
description: 'Gets the minimum value from a number-only array.',
description:
'Returns the smallest number in the array. Throws an error if there are any non-numbers.',
examples: [{ example: '[12, 1, 5].min()', evaluated: '1' }],
returnType: 'number',
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/arrays/#array-min',
};
randomItem.doc = {
name: 'randomItem',
description: 'Returns a random element from an array.',
returnType: 'Element',
description: 'Returns a randomly-chosen element from the array',
examples: [
{ example: "['quick', 'brown', 'fox'].randomItem()", evaluated: "'brown'" },
{ example: "['quick', 'brown', 'fox'].randomItem()", evaluated: "'quick'" },
],
returnType: 'any',
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/arrays/#array-randomItem',
};
sum.doc = {
name: 'sum',
description: 'Returns the total sum all the values in an array of parsable numbers.',
description:
'Returns the total of all the numbers in the array. Throws an error if there are any non-numbers.',
examples: [{ example: '[12, 1, 5].sum()', evaluated: '18' }],
returnType: 'number',
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/arrays/#array-sum',
};
chunk.doc = {
name: 'chunk',
description: 'Splits arrays into chunks with a length of `size`.',
description: 'Splits the array into an array of sub-arrays, each with the given length',
examples: [{ example: '[1, 2, 3, 4, 5, 6].chunk(2)', evaluated: '[[1,2],[3,4],[5,6]]' }],
returnType: 'Array',
args: [{ name: 'size', type: 'number' }],
args: [
{
name: 'length',
optional: false,
description: 'The number of elements in each chunk',
type: 'number',
},
],
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/arrays/#array-chunk',
};
difference.doc = {
name: 'difference',
description:
'Compares two arrays. Returns all elements in the base array that arent present in `arr`.',
"Compares two arrays. Returns all elements in the base array that aren't present\nin <code>otherArray</code>.",
examples: [{ example: '[1, 2, 3].difference([2, 3])', evaluated: '[1]' }],
returnType: 'Array',
args: [{ name: 'arr', type: 'Array' }],
args: [
{
name: 'otherArray',
optional: false,
description: 'The array to compare to the base array',
type: 'Array',
},
],
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/arrays/#array-difference',
};
intersection.doc = {
name: 'intersection',
description:
'Compares two arrays. Returns all elements in the base array that are present in `arr`.',
'Compares two arrays. Returns all elements in the base array that are also present in the other array.',
examples: [{ example: '[1, 2].intersection([2, 3])', evaluated: '[2]' }],
returnType: 'Array',
args: [{ name: 'arr', type: 'Array' }],
args: [
{
name: 'otherArray',
optional: false,
description: 'The array to compare to the base array',
type: 'Array',
},
],
docURL:
'https://docs.n8n.io/code/builtin/data-transformation-functions/arrays/#array-intersection',
};
@ -440,37 +480,72 @@ intersection.doc = {
merge.doc = {
name: 'merge',
description:
'Merges two Object-arrays into one array by merging the key-value pairs of each element.',
returnType: 'array',
args: [{ name: 'arr', type: 'Array' }],
'Merges two Object-arrays into one object by merging the key-value pairs of each element.',
examples: [
{
example:
"[{ name: 'Nathan' }, { age: 42 }].merge([{ city: 'Berlin' }, { country: 'Germany' }])",
evaluated: "{ name: 'Nathan', age: 42, city: 'Berlin', country: 'Germany' }",
},
],
returnType: 'Object',
args: [
{
name: 'otherArray',
optional: false,
description: 'The array to merge into the base array',
type: 'Array',
},
],
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/arrays/#array-merge',
};
pluck.doc = {
name: 'pluck',
description: 'Returns an array of Objects where the key is equal the given `fieldName`s.',
description:
'Returns an array containing the values of the given field(s) in each Object of the array. Ignores any array elements that arent Objects or dont have a key matching the field name(s) provided.',
examples: [
{
example: "[{ name: 'Nathan', age: 42 },{ name: 'Jan', city: 'Berlin' }].pluck('name')",
evaluated: '["Nathan", "Jan"]',
},
{
example: "[{ name: 'Nathan', age: 42 },{ name: 'Jan', city: 'Berlin' }].pluck('age')",
evaluated: '[42]',
},
],
returnType: 'Array',
args: [
{ name: 'fieldName1', type: 'string' },
{ name: 'fieldName1?', type: 'string' },
{ name: '...' },
{ name: 'fieldNameN?', type: 'string' },
{
name: 'fieldNames',
optional: false,
variadic: true,
description: 'The keys to retrieve the value of',
type: 'string',
},
],
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/arrays/#array-pluck',
};
renameKeys.doc = {
name: 'renameKeys',
description: 'Renames all matching keys in the array.',
description:
'Changes all matching keys (field names) of any Objects in the array. Rename more than one key by\nadding extra arguments, i.e. <code>from1, to1, from2, to2, ...</code>.',
examples: [
{
example: "[{ name: 'bob' }, { name: 'meg' }].renameKeys('name', 'x')",
evaluated: "[{ x: 'bob' }, { x: 'meg' }]",
},
],
returnType: 'Array',
args: [
{ name: 'from1', type: 'string' },
{ name: 'to1', type: 'string' },
{ name: 'from2?', type: 'string' },
{ name: 'to2?', type: 'string' },
{ name: '...' },
{ name: 'fromN?', type: 'string' },
{ name: 'toN?', type: 'string' },
{
name: 'from',
optional: false,
description: 'The key to rename',
type: 'string',
},
{ name: 'to', optional: false, description: 'The new key name', type: 'string' },
],
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/arrays/#array-renameKeys',
};
@ -478,39 +553,117 @@ renameKeys.doc = {
smartJoin.doc = {
name: 'smartJoin',
description:
'Operates on an array of objects where each object contains key-value pairs. Creates a new object containing key-value pairs, where the key is the value of the first pair, and the value is the value of the second pair. Removes non-matching and empty values and trims any whitespace before joining.',
returnType: 'Array',
'Creates a single Object from an array of Objects. Each Object in the array provides one field for the returned Object. Each Object in the array must contain a field with the key name and a field with the value.',
examples: [
{
example:
"[{ field: 'age', value: 2 }, { field: 'city', value: 'Berlin' }].smartJoin('field', 'value')",
evaluated: "{ age: 2, city: 'Berlin' }",
},
],
returnType: 'Object',
args: [
{ name: 'keyField', type: 'string' },
{ name: 'nameField', type: 'string' },
{
name: 'keyField',
optional: false,
description: 'The field in each Object containing the key name',
type: 'string',
},
{
name: 'nameField',
optional: false,
description: 'The field in each Object containing the value',
type: 'string',
},
],
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/arrays/#array-smartJoin',
};
union.doc = {
name: 'union',
description: 'Concatenates two arrays and then removes duplicates.',
description: 'Concatenates two arrays and then removes any duplicates',
examples: [{ example: '[1, 2].union([2, 3])', evaluated: '[1, 2, 3]' }],
returnType: 'Array',
args: [{ name: 'arr', type: 'Array' }],
args: [
{
name: 'otherArray',
optional: false,
description: 'The array to union with the base array',
type: 'Array',
},
],
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/arrays/#array-union',
};
unique.doc = {
name: 'unique',
description: 'Remove duplicates from an array. ',
returnType: 'Element',
description: 'Removes any duplicate elements from the array',
examples: [
{ example: "['quick', 'brown', 'quick'].unique()", evaluated: "['quick', 'brown']" },
{
example: "[{ name: 'Nathan', age: 42 }, { name: 'Nathan', age: 22 }].unique()",
evaluated: "[{ name: 'Nathan', age: 42 }, { name: 'Nathan', age: 22 }]",
},
{
example: "[{ name: 'Nathan', age: 42 }, { name: 'Nathan', age: 22 }].unique('name')",
evaluated: "[{ name: 'Nathan', age: 42 }]",
},
],
returnType: 'any',
aliases: ['removeDuplicates'],
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/arrays/#array-unique',
args: [
{
name: 'fieldNames',
optional: false,
variadic: true,
description: 'The object keys to check for equality',
type: 'any',
},
],
};
toJsonString.doc = {
name: 'toJsonString',
description: 'Converts an array to a JSON string',
description:
"Converts the array to a JSON string. The same as JavaScript's <code>JSON.stringify()</code>.",
examples: [
{
example: "['quick', 'brown', 'fox'].toJsonString()",
evaluated: '\'["quick","brown","fox"]\'',
},
],
docURL:
'https://docs.n8n.io/code/builtin/data-transformation-functions/arrays/#array-toJsonString',
returnType: 'string',
};
append.doc = {
name: 'append',
description:
'Adds new elements to the end of the array. Similar to <code>push()</code>, but returns the modified array. Consider using spread syntax instead (see examples).',
examples: [
{ example: "['forget', 'me'].append('not')", evaluated: "['forget', 'me', 'not']" },
{ example: '[9, 0, 2].append(1, 0)', evaluated: '[9, 0, 2, 1, 0]' },
{
example: '[...[9, 0, 2], 1, 0]',
evaluated: '[9, 0, 2, 1, 0]',
description: 'Consider using spread syntax instead',
},
],
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/arrays/#array-append',
returnType: 'Array',
args: [
{
name: 'elements',
optional: false,
variadic: true,
description: 'The elements to append, in order',
type: 'any',
},
],
};
const removeDuplicates: Extension = unique.bind({});
removeDuplicates.doc = { ...unique.doc, hidden: true };
@ -537,6 +690,7 @@ export const arrayExtensions: ExtensionMap = {
union,
difference,
intersection,
append,
toJsonString,
toInt,
toFloat,

View file

@ -19,6 +19,8 @@ export type DocMetadataArgument = {
variadic?: boolean;
description?: string;
default?: string;
// Function arguments have nested arguments
args?: DocMetadataArgument[];
};
export type DocMetadataExample = {
example: string;

View file

@ -62,7 +62,7 @@ function toFloat(value: number) {
}
type DateTimeFormat = 'ms' | 's' | 'us' | 'excel';
function toDateTime(value: number, extraArgs: [DateTimeFormat]) {
export function toDateTime(value: number, extraArgs: [DateTimeFormat]) {
const [valueFormat = 'ms'] = extraArgs;
if (!['ms', 's', 'us', 'excel'].includes(valueFormat)) {

View file

@ -110,14 +110,22 @@ export function toDateTime() {
isEmpty.doc = {
name: 'isEmpty',
description: 'Checks if the Object has no key-value pairs.',
description: 'Returns <code>true</code> if the Object has no keys (fields) set',
examples: [
{ example: "({'name': 'Nathan'}).isEmpty()", evaluated: 'false' },
{ example: '({}).isEmpty()', evaluated: 'true' },
],
returnType: 'boolean',
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/objects/#object-isEmpty',
};
isNotEmpty.doc = {
name: 'isNotEmpty',
description: 'Checks if the Object has key-value pairs.',
description: 'Returns <code>true</code> if the Object has at least one key (field) set',
examples: [
{ example: "({'name': 'Nathan'}).isNotEmpty()", evaluated: 'true' },
{ example: '({}).isNotEmpty()', evaluated: 'false' },
],
returnType: 'boolean',
docURL:
'https://docs.n8n.io/code/builtin/data-transformation-functions/objects/#object-isNotEmpty',
@ -125,14 +133,23 @@ isNotEmpty.doc = {
compact.doc = {
name: 'compact',
description: 'Removes empty values from an Object.',
returnType: 'boolean',
description:
'Removes all fields that have empty values, i.e. are <code>null</code>, <code>undefined</code>, <code>"nil"</code> or <code>""</code>',
examples: [{ example: "({ x: null, y: 2, z: '' }).compact()", evaluated: '{ y: 2 }' }],
returnType: 'Object',
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/objects/#object-compact',
};
urlEncode.doc = {
name: 'urlEncode',
description: 'Transforms an Object into a URL parameter list. Only top-level keys are supported.',
description:
"Generates a URL parameter string from the Object's keys and values. Only top-level keys are supported.",
examples: [
{
example: "({ name: 'Mr Nathan', city: 'hanoi' }).urlEncode()",
evaluated: "'name=Mr+Nathan&city=hanoi'",
},
],
returnType: 'string',
docURL:
'https://docs.n8n.io/code/builtin/data-transformation-functions/objects/#object-urlEncode',
@ -140,17 +157,43 @@ urlEncode.doc = {
hasField.doc = {
name: 'hasField',
description: 'Checks if the Object has a given field. Only top-level keys are supported.',
description:
'Returns <code>true</code> if there is a field called <code>name</code>. Only checks top-level keys. Comparison is case-sensitive.',
examples: [
{ example: "({ name: 'Nathan', age: 42 }).hasField('name')", evaluated: 'true' },
{ example: "({ name: 'Nathan', age: 42 }).hasField('Name')", evaluated: 'false' },
{ example: "({ name: 'Nathan', age: 42 }).hasField('inventedField')", evaluated: 'false' },
],
returnType: 'boolean',
args: [{ name: 'fieldName', type: 'string' }],
args: [
{
name: 'name',
optional: false,
description: 'The name of the key to search for',
type: 'string',
},
],
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/objects/#object-hasField',
};
removeField.doc = {
name: 'removeField',
description: 'Removes a given field from the Object. Only top-level fields are supported.',
returnType: 'object',
args: [{ name: 'key', type: 'string' }],
description: "Removes a field from the Object. The same as JavaScript's <code>delete</code>.",
examples: [
{
example: "({ name: 'Nathan', city: 'hanoi' }).removeField('name')",
evaluated: "{ city: 'hanoi' }",
},
],
returnType: 'Object',
args: [
{
name: 'key',
optional: false,
description: 'The name of the field to remove',
type: 'string',
},
],
docURL:
'https://docs.n8n.io/code/builtin/data-transformation-functions/objects/#object-removeField',
};
@ -158,39 +201,95 @@ removeField.doc = {
removeFieldsContaining.doc = {
name: 'removeFieldsContaining',
description:
'Removes fields with a given value from the Object. Only top-level values are supported.',
returnType: 'object',
args: [{ name: 'value', type: 'string' }],
"Removes keys (fields) whose values at least partly match the given <code>value</code>. Comparison is case-sensitive. Fields that aren't strings are always kept.",
examples: [
{
example: "({ name: 'Mr Nathan', city: 'hanoi', age: 42 }).removeFieldsContaining('Nathan')",
evaluated: "{ city: 'hanoi', age: 42 }",
},
{
example: "({ name: 'Mr Nathan', city: 'hanoi', age: 42 }).removeFieldsContaining('Han')",
evaluated: '{ age: 42 }',
},
{
example: "({ name: 'Mr Nathan', city: 'hanoi', age: 42 }).removeFieldsContaining('nathan')",
evaluated: "{ name: 'Mr Nathan', city: 'hanoi', age: 42 }",
},
],
returnType: 'Object',
args: [
{
name: 'value',
optional: false,
description: 'The text that a value must contain in order to be removed',
type: 'string',
},
],
docURL:
'https://docs.n8n.io/code/builtin/data-transformation-functions/objects/#object-removeFieldsContaining',
};
keepFieldsContaining.doc = {
name: 'keepFieldsContaining',
description: 'Removes fields that do not match the given value from the Object.',
returnType: 'object',
args: [{ name: 'value', type: 'string' }],
description:
"Removes any fields whose values don't at least partly match the given <code>value</code>. Comparison is case-sensitive. Fields that aren't strings will always be removed.",
examples: [
{
example: "({ name: 'Mr Nathan', city: 'hanoi', age: 42 }).keepFieldsContaining('Nathan')",
evaluated: "{ name: 'Mr Nathan' }",
},
{
example: "({ name: 'Mr Nathan', city: 'hanoi', age: 42 }).keepFieldsContaining('nathan')",
evaluated: '{}',
},
{
example: "({ name: 'Mr Nathan', city: 'hanoi', age: 42 }).keepFieldsContaining('han')",
evaluated: "{ name: 'Mr Nathan', city: 'hanoi' }",
},
],
returnType: 'Object',
args: [
{
name: 'value',
optional: false,
description: 'The text that a value must contain in order to be kept',
type: 'string',
},
],
docURL:
'https://docs.n8n.io/code/builtin/data-transformation-functions/objects/#object-keepFieldsContaining',
};
keys.doc = {
name: 'keys',
description: "Returns an array of a given object's own enumerable string-keyed property names.",
description:
"Returns an array with all the field names (keys) the Object contains. The same as JavaScript's <code>Object.keys(obj)</code>.",
examples: [{ example: "({ name: 'Mr Nathan', age: 42 }).keys()", evaluated: "['name', 'age']" }],
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/objects/#object-keys',
returnType: 'Array',
};
values.doc = {
name: 'values',
description: "Returns an array of a given object's own enumerable string-keyed property values.",
description:
"Returns an array with all the values of the fields the Object contains. The same as JavaScript's <code>Object.values(obj)</code>.",
examples: [
{ example: "({ name: 'Mr Nathan', age: 42 }).values()", evaluated: "['Mr Nathan', 42]" },
],
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/objects/#object-values',
returnType: 'Array',
};
toJsonString.doc = {
name: 'toJsonString',
description: 'Converts an object to a JSON string',
description:
"Converts the Object to a JSON string. Similar to JavaScript's <code>JSON.stringify()</code>.",
examples: [
{
example: "({ name: 'Mr Nathan', age: 42 }).toJsonString()",
evaluated: '\'{"name":"Nathan","age":42}\'',
},
],
docURL:
'https://docs.n8n.io/code/builtin/data-transformation-functions/objects/#object-toJsonString',
returnType: 'string',

View file

@ -5,8 +5,9 @@ import { titleCase } from 'title-case';
import type { Extension, ExtensionMap } from './Extensions';
import { transliterate } from 'transliteration';
import { ExpressionExtensionError } from '../errors/expression-extension.error';
import type { DateTime } from 'luxon';
import { DateTime } from 'luxon';
import { tryToParseDateTime } from '../TypeValidation';
import { toDateTime as numberToDateTime } from './NumberExtensions';
export const SupportedHashAlgorithms = [
'md5',
@ -216,8 +217,22 @@ function toDate(value: string): Date {
return date;
}
function toDateTime(value: string): DateTime {
function toDateTime(value: string, extraArgs: [string]): DateTime {
try {
const [valueFormat] = extraArgs;
if (valueFormat) {
if (
valueFormat === 'ms' ||
valueFormat === 's' ||
valueFormat === 'us' ||
valueFormat === 'excel'
) {
return numberToDateTime(Number(value), [valueFormat]);
}
return DateTime.fromFormat(value, valueFormat);
}
return tryToParseDateTime(value);
} catch (error) {
throw new ExpressionExtensionError('cannot convert to Luxon DateTime');
@ -454,7 +469,17 @@ toDateTime.doc = {
{ example: '"2024-03-29T18:06:31.798+01:00".toDateTime()' },
{ example: '"Fri, 29 Mar 2024 18:08:01 +0100".toDateTime()' },
{ example: '"20240329".toDateTime()' },
{ example: '"1711732132990".toDateTime()' },
{ example: '"1711732132990".toDateTime("ms")' },
{ example: '"31-01-2024".toDateTime("dd-MM-yyyy")' },
],
args: [
{
name: 'format',
optional: true,
description:
'The format of the date string. Options are <code>ms</code> (for Unix timestamp in milliseconds), <code>s</code> (for Unix timestamp in seconds), <code>us</code> (for Unix timestamp in microseconds) or <code>excel</code> (for days since 1900). Custom formats can be specified using <a href="https://moment.github.io/luxon/#/formatting?id=table-of-tokens">Luxon tokens</a>.',
type: 'string',
},
],
};

View file

@ -6,7 +6,8 @@ export const arrayMethods: NativeDoc = {
length: {
doc: {
name: 'length',
description: 'Returns the number of elements in the Array.',
description: 'The number of elements in the array',
examples: [{ example: "['Bob', 'Bill', 'Nat'].length", evaluated: '3' }],
docURL:
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/length',
returnType: 'number',
@ -17,37 +18,159 @@ export const arrayMethods: NativeDoc = {
concat: {
doc: {
name: 'concat',
description: 'Merges two or more arrays into one array.',
description: 'Joins one or more arrays onto the end of the base array',
examples: [
{
example: "['Nathan', 'Jan'].concat(['Steve', 'Bill'])",
evaluated: "['Nathan', 'Jan', 'Steve', 'Bill']",
},
{
example: "[5, 4].concat([100, 101], ['a', 'b'])",
evaluated: "[5, 4, 100, 101, 'a', 'b']",
},
],
docURL:
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/concat',
returnType: 'Array',
args: [
{ name: 'arr1', type: 'Array' },
{ name: 'arr2', type: 'Array' },
{ name: '...' },
{ name: 'arrN', type: 'Array' },
{
name: 'arrays',
variadic: true,
description: 'The arrays to be joined on the end of the base array, in order',
type: 'Array',
},
],
},
},
filter: {
doc: {
name: 'filter',
description: 'Returns an array only containing the elements that pass the test `fn`.',
description:
'Returns an array with only the elements satisfying a condition. The condition is a function that returns <code>true</code> or <code>false</code>.',
examples: [
{
example: '[12, 33, 16, 40].filter(age => age > 18)',
evaluated: '[33, 40]',
description: 'Keep ages over 18 (using arrow function notation)',
},
{
example: "['Nathan', 'Bob', 'Sebastian'].filter(name => name.length < 5)",
evaluated: "['Bob']",
description: 'Keep names under 5 letters long (using arrow function notation)',
},
{
example:
"['Nathan', 'Bob', 'Sebastian'].filter(function(name) { return name.length < 5 })",
evaluated: "['Bob']",
description: 'Or using traditional function notation',
},
{
example: '[1, 7, 3, 10, 5].filter((num, index) => index % 2 !== 0)',
evaluated: '[7, 10]',
description: 'Keep numbers at odd indexes',
},
],
docURL:
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter',
returnType: 'Array',
args: [{ name: 'fn', type: 'Function' }],
args: [
{
name: 'function',
description:
'A function to run for each array element. If it returns <code>true</code>, the element will be kept. Consider using <a target="_blank" href=”https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions”>arrow function notation</a> to save space.',
type: 'Function',
default: 'item => true',
args: [
{
name: 'element',
description: 'The value of the current element',
type: 'any',
},
{
name: 'index',
optional: true,
description: 'The position of the current element in the array (starting at 0)',
type: 'number',
},
{
name: 'array',
optional: true,
description: 'The array being processed. Rarely needed.',
type: 'Array',
},
{
name: 'thisValue',
optional: true,
description:
'A value passed to the function as its <code>this</code> value. Rarely needed.',
type: 'any',
},
],
},
],
},
},
find: {
doc: {
name: 'find',
description:
'Returns the first element in the provided array that passes the test `fn`. If no values satisfy the testing function, `undefined` is returned.',
'Returns the first element from the array that satisfies the provided condition. The condition is a function that returns <code>true</code> or <code>false</code>. Returns <code>undefined</code> if no matches are found.\n\nIf you need all matching elements, use <code>filter()</code>.',
examples: [
{
example: '[12, 33, 16, 40].find(age => age > 18)',
evaluated: '33',
description: 'Find first age over 18 (using arrow function notation)',
},
{
example: "['Nathan', 'Bob', 'Sebastian'].find(name => name.length < 5)",
evaluated: "'Bob'",
description: 'Find first name under 5 letters long (using arrow function notation)',
},
{
example:
"['Nathan', 'Bob', 'Sebastian'].find(function(name) { return name.length < 5 })",
evaluated: "'Bob'",
description: 'Or using traditional function notation',
},
],
docURL:
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find',
returnType: 'Array|undefined',
args: [{ name: 'fn', type: 'Function' }],
returnType: 'Array | undefined',
args: [
{
name: 'function',
description:
'A function to run for each array element. As soon as it returns <code>true</code>, that element will be returned. Consider using <a target="_blank" href=”https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions”>arrow function notation</a> to save space.',
type: 'Function',
default: 'item => true',
args: [
{
name: 'element',
description: 'The value of the current element',
type: 'any',
},
{
name: 'index',
optional: true,
description: 'The position of the current element in the array (starting at 0)',
type: 'number',
},
{
name: 'array',
optional: true,
description: 'The array being processed. Rarely needed.',
type: 'Array',
},
{
name: 'thisValue',
optional: true,
description:
'A value passed to the function as its <code>this</code> value. Rarely needed.',
type: 'any',
},
],
},
],
},
},
findIndex: {
@ -69,7 +192,7 @@ export const arrayMethods: NativeDoc = {
description: 'Returns the value of the last element that passes the test `fn`.',
docURL:
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findLast',
returnType: 'Element|undefined',
returnType: 'any | undefined',
args: [{ name: 'fn', type: 'Function' }],
},
},
@ -89,26 +212,54 @@ export const arrayMethods: NativeDoc = {
doc: {
name: 'indexOf',
description:
'Returns the first index at which a given element can be found in the array, or -1 if it is not present.',
"Returns the position of the first matching element in the array, or -1 if the element isn't found. Positions start at 0.",
examples: [
{ example: "['Bob', 'Bill', 'Nat'].indexOf('Nat')", evaluated: '2' },
{ example: "['Bob', 'Bill', 'Nat'].indexOf('Nathan')", evaluated: '-1' },
],
docURL:
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf',
returnType: 'number',
args: [
{ name: 'searchElement', type: 'string|number' },
{ name: 'fromIndex?', type: 'number' },
{
name: 'element',
description: 'The value to look for',
type: 'any',
},
{
name: 'start',
optional: true,
description: 'The index to start looking from',
default: '0',
type: 'number',
},
],
},
},
includes: {
doc: {
name: 'includes',
description: 'Checks if an array includes a certain value among its entries.',
description: 'Returns <code>true</code> if the array contains the specified element',
examples: [
{ example: "['Bob', 'Bill', 'Nat'].indexOf('Nat')", evaluated: 'true' },
{ example: "['Bob', 'Bill', 'Nat'].indexOf('Nathan')", evaluated: 'false' },
],
docURL:
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes',
returnType: 'boolean',
args: [
{ name: 'searchElement', type: 'Element' },
{ name: 'fromIndex?', type: 'number' },
{
name: 'element',
description: 'The value to search the array for',
type: 'any',
},
{
name: 'start',
optional: true,
description: 'The index to start looking from',
default: '0',
type: 'number',
},
],
},
},
@ -116,28 +267,95 @@ export const arrayMethods: NativeDoc = {
doc: {
name: 'join',
description:
'Returns a string that concatenates all of the elements in an array, separated by `separator`, which defaults to comma.',
'Merges all elements of the array into a single string, with an optional separator between each element.\n\nThe opposite of <code>String.split()</code>.',
examples: [
{ example: "['Wind', 'Water', 'Fire'].join(' + ')", evaluated: "'Wind + Water + Fire'" },
{ example: "['Wind', 'Water', 'Fire'].join()", evaluated: "'Wind,Water,Fire'" },
{ example: "['Wind', 'Water', 'Fire'].join('')", evaluated: "'WindWaterFire'" },
],
docURL:
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join',
returnType: 'string',
args: [{ name: 'separator?', type: 'string' }],
args: [
{
name: 'separator',
optional: true,
description: 'The character(s) to insert between each element',
default: "','",
type: 'string',
},
],
},
},
map: {
doc: {
name: 'map',
description: 'Returns an array containing the results of calling `fn` on every element.',
description:
'Creates a new array by applying a function to each element of the original array',
examples: [
{
example: '[12, 33, 16].map(num => num * 2)',
evaluated: '[24, 66, 32]',
description: 'Double all numbers (using arrow function notation)',
},
{
example: "['hello', 'old', 'chap'].map(word => word.toUpperCase())",
evaluated: "['HELLO', 'OLD', 'CHAP']]",
description: 'Convert elements to uppercase (using arrow function notation)',
},
{
example: "['hello', 'old', 'chap'].map(function(word) { return word.toUpperCase() })",
evaluated: "['HELLO', 'OLD', 'CHAP']]",
description: 'Or using traditional function notation',
},
],
docURL:
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map',
returnType: 'Array',
args: [{ name: 'fn', type: 'Function' }],
args: [
{
name: 'function',
description:
'A function to run for each array element. In the new array, the output of this function takes the place of the element. Consider using <a target="_blank" href=”https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions”>arrow function notation</a> to save space.',
type: 'Function',
default: 'item => item',
args: [
{
name: 'element',
description: 'The value of the current element',
type: 'any',
},
{
name: 'index',
optional: true,
description: 'The position of the current element in the array (starting at 0)',
type: 'number',
},
{
name: 'array',
optional: true,
description: 'The array being processed. Rarely needed.',
type: 'Array',
},
{
name: 'thisValue',
optional: true,
description:
'A value passed to the function as its <code>this</code> value. Rarely needed.',
type: 'any',
},
],
},
],
},
},
reverse: {
doc: {
name: 'reverse',
description:
'Reverses an array and returns it. The first array element now becomes the last, and the last array element becomes the first.',
description: 'Reverses the order of the elements in the array',
examples: [
{ example: "['dog', 'bites', 'man'].reverse()", evaluated: "['man', 'bites', 'dog']" },
],
docURL:
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse',
returnType: 'Array',
@ -151,20 +369,78 @@ export const arrayMethods: NativeDoc = {
docURL:
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce',
returnType: 'any',
args: [{ name: 'fn', type: 'Function' }],
args: [
{
name: 'function',
description:
'A function to run for each array element. Takes the accumulated result and the current element, and returns a new accumulated result. Consider using <a target="_blank" href=”https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions”>arrow function notation</a> to save space.',
type: 'Function',
default: 'item => item',
args: [
{
name: 'prevResult',
description:
'The accumulated result from applying the function to previous elements. When processing the first element, its set to <code>initResult</code> (or the first array element if not specified).',
type: 'any',
},
{
name: 'currentElem',
description: 'The value in the array currently being processed',
type: 'any',
},
{
name: 'index',
optional: true,
description: 'The position of the current element in the array (starting at 0)',
type: 'number',
},
{
name: 'array',
optional: true,
description: 'The array being processed. Rarely needed.',
type: 'Array',
},
],
},
{
name: 'initResult',
optional: true,
description:
"The initial value of the prevResult, used when calling the function on the first array element. When not specified it's set to the first array element, and the first function call is on the second array element instead of the first.",
type: 'any',
},
],
},
},
slice: {
doc: {
name: 'slice',
description:
'Returns a section of an Array. `end` defaults to the length of the Array if not given.',
'Returns a portion of the array, from the <code>start</code> index up to (but not including) the <code>end</code> index. Indexes start at 0.',
examples: [
{ example: '[1, 2, 3, 4, 5].slice(2, 4)', evaluated: '[3, 4]' },
{ example: '[1, 2, 3, 4, 5].slice(2)', evaluated: '[3, 4, 5]' },
{ example: '[1, 2, 3, 4, 5].slice(-2)', evaluated: '[4, 5]' },
],
docURL:
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice',
returnType: 'Array',
args: [
{ name: 'start', type: 'number' },
{ name: 'end?', type: 'number' },
{
name: 'start',
optional: true,
description:
'The position to start from. Positions start at 0. Negative numbers count back from the end of the array.',
default: '0',
type: 'number',
},
{
name: 'end',
optional: true,
description:
'The position to select up to. The element at the end position is not included. Negative numbers select from the end of the array. If omitted, will extract to the end of the array.',
type: 'number',
},
],
},
},
@ -172,11 +448,62 @@ export const arrayMethods: NativeDoc = {
doc: {
name: 'sort',
description:
'Returns a sorted array. The default sort order is ascending, built upon converting the elements into strings.',
'Reorders the elements of the array. For sorting strings alphabetically, no parameter is required. For sorting numbers or Objects, see examples.',
examples: [
{
example: "['d', 'a', 'c', 'b'].sort()",
evaluated: "['a', 'b', 'c', 'd']",
description: 'No need for a param when sorting strings',
},
{
example: '[4, 2, 1, 3].sort((a, b) => (a - b))',
evaluated: '[1, 2, 3, 4]',
description: 'To sort numbers, you must use a function',
},
{
example: '[4, 2, 1, 3].sort(function(a, b) { return a - b })',
evaluated: '[1, 2, 3, 4]',
description: 'Or using traditional function notation',
},
{ example: 'Sort in reverse alphabetical order' },
{ example: "arr = ['d', 'a', 'c', 'b']" },
{
example: 'arr.sort((a, b) => b.localeCompare(a))',
evaluated: "['d', 'c', 'b', 'a']",
description: 'Sort in reverse alphabetical order',
},
{
example:
"[{name:'Zak'}, {name:'Abe'}, {name:'Bob'}].sort((a, b) => a.name.localeCompare(b.name))",
evaluated: "[{name:'Abe'}, {name:'Bob'}, {name:'Zak'}]",
description: 'Sort array of objects by a property',
},
],
docURL:
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort',
returnType: 'Array',
args: [{ name: 'fn?', type: 'Function' }],
args: [
{
name: 'compare',
optional: true,
description:
'A function to compare two array elements and return a number indicating which one comes first:\n<b>Return < 0</b>: <code>a</code> comes before <code>b</code>\n<b>Return 0</b>: <code>a</code> and <code>b</code> are equal (leave order unchanged)\n<b>Return > 0</b>: <code>b</code> comes before <code>a</code>\n\nIf no function is specified, converts all values to strings and compares their character codes.',
default: '""',
type: '(a, b) => number',
args: [
{
name: 'a',
description: 'The first element to compare in the function',
type: 'any',
},
{
name: 'b',
description: 'The second element to compare in the function',
type: 'any',
},
],
},
],
},
},
splice: {
@ -210,10 +537,49 @@ export const arrayMethods: NativeDoc = {
doc: {
name: 'toSpliced',
description:
'Returns a new array with some elements removed and/or replaced at a given index. <code>toSpliced()</code> is the copying version of the <code>splice()</code> method',
'Adds and/or removes array elements at a given position. \n\nSee also <code>slice()</code> and <code>append()</code>.',
examples: [
{
example: "['Jan', 'Mar'.toSpliced(1, 0, 'Feb')",
evaluated: "['Jan', 'Feb', 'Mar']",
description: 'Insert element at index 1',
},
{
example: '["don\'t", "make", "me", "do", "this"].toSpliced(1, 2)',
evaluated: '["don\'t", "do", "this"]',
description: 'Delete 2 elements starting at index 1',
},
{
example: '["don\'t", "be", "evil"].toSpliced(1, 2, "eat", "slugs")',
evaluated: '["don\'t", "eat", "slugs"]',
description: 'Replace 2 elements starting at index 1',
},
],
docURL:
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toSpliced',
returnType: 'Array',
args: [
{
name: 'start',
description:
'The index (position) to add or remove elements at. New elements are inserted before the element at this index. A negative index counts back from the end of the array. ',
type: 'number',
},
{
name: 'deleteCount',
optional: true,
description:
'The number of elements to remove. If omitted, removes all elements from the <code>start</code> index onwards.',
type: 'number',
},
{
name: 'elements',
optional: true,
variadic: true,
description: 'The elements to be added, in order',
type: 'any',
},
],
},
},
},

View file

@ -92,6 +92,17 @@ describe('Data Transformation Functions', () => {
).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);
});
@ -242,6 +253,10 @@ describe('Data Transformation Functions', () => {
);
});
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();

View file

@ -253,6 +253,9 @@ describe('Data Transformation Functions', () => {
);
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);
expect(() => evaluate('={{ "hi".toDateTime() }}')).toThrowError(
new ExpressionExtensionError('cannot convert to Luxon DateTime'),
);