feat(editor): Add missing extension methods for expressions (#8845)

This commit is contained in:
Elias Meire
2024-03-20 12:05:54 +01:00
committed by GitHub
parent 7176cd1407
commit 5e84c2ab89
28 changed files with 809 additions and 39 deletions

View File

@@ -320,6 +320,26 @@ function intersection(value: unknown[], extraArgs: unknown[][]): unknown[] {
return unique(newArr, []);
}
export function toJsonString(value: unknown[]) {
return JSON.stringify(value);
}
export function toInt() {
return undefined;
}
export function toFloat() {
return undefined;
}
export function toBoolean() {
return undefined;
}
export function toDateTime() {
return undefined;
}
average.doc = {
name: 'average',
description: 'Returns the mean average of all values in the array.',
@@ -483,6 +503,14 @@ unique.doc = {
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/arrays/#array-unique',
};
toJsonString.doc = {
name: 'toJsonString',
description: 'Converts an array to a JSON string',
docURL:
'https://docs.n8n.io/code/builtin/data-transformation-functions/arrays/#array-toJsonString',
returnType: 'string',
};
export const arrayExtensions: ExtensionMap = {
typeName: 'Array',
functions: {
@@ -506,5 +534,10 @@ export const arrayExtensions: ExtensionMap = {
union,
difference,
intersection,
toJsonString,
toInt,
toFloat,
toBoolean,
toDateTime,
},
};

View File

@@ -0,0 +1,35 @@
import type { ExtensionMap } from './Extensions';
export function toBoolean(value: boolean) {
return value;
}
export function toInt(value: boolean) {
return value ? 1 : 0;
}
export function toFloat(value: boolean) {
return value ? 1 : 0;
}
export function toDateTime() {
return undefined;
}
toInt.doc = {
name: 'toInt',
description: 'Converts a boolean to an integer. `false` is 0, `true` is 1.',
section: 'cast',
returnType: 'boolean',
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/booleans/#boolean-toInt',
};
export const booleanExtensions: ExtensionMap = {
typeName: 'Boolean',
functions: {
toBoolean,
toInt,
toFloat,
toDateTime,
},
};

View File

@@ -216,6 +216,25 @@ function plus(
return DateTime.fromJSDate(date).plus(duration).toJSDate();
}
function toDateTime(date: Date | DateTime): DateTime {
if (isDateTime(date)) return date;
return DateTime.fromJSDate(date);
}
function toInt(date: Date | DateTime): number {
if (isDateTime(date)) {
return date.toMillis();
}
return date.getTime();
}
const toFloat = toInt;
function toBoolean() {
return undefined;
}
endOfMonth.doc = {
name: 'endOfMonth',
returnType: 'Date',
@@ -267,7 +286,7 @@ format.doc = {
description: 'Formats a Date in the given structure.',
returnType: 'string',
section: 'format',
args: [{ name: 'fmt', type: 'TimeFormat' }],
args: [{ name: 'fmt', default: 'yyyy-MM-dd', type: 'TimeFormat' }],
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/dates/#date-format',
};
@@ -295,6 +314,15 @@ isInLast.doc = {
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/dates/#date-isInLast',
};
toDateTime.doc = {
name: 'toDateTime',
description: 'Convert a JavaScript Date to a Luxon DateTime.',
section: 'query',
returnType: 'DateTime',
hidden: true,
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/dates/#date-toDateTime',
};
minus.doc = {
name: 'minus',
description: 'Subtracts a given time period from a Date. Default unit is `milliseconds`.',
@@ -332,5 +360,9 @@ export const dateExtensions: ExtensionMap = {
minus,
plus,
format,
toDateTime,
toInt,
toFloat,
toBoolean,
},
};

View File

@@ -15,6 +15,7 @@ import type { ExpressionKind } from 'ast-types/gen/kinds';
import type { ExpressionChunk, ExpressionCode } from './ExpressionParser';
import { joinExpression, splitExpression } from './ExpressionParser';
import { booleanExtensions } from './BooleanExtensions';
const EXPRESSION_EXTENDER = 'extend';
const EXPRESSION_EXTENDER_OPTIONAL = 'extendOptional';
@@ -33,6 +34,7 @@ export const EXTENSION_OBJECTS = [
numberExtensions,
objectExtensions,
stringExtensions,
booleanExtensions,
];
// eslint-disable-next-line @typescript-eslint/ban-types
@@ -48,6 +50,7 @@ const EXPRESSION_EXTENSION_METHODS = Array.from(
...Object.keys(dateExtensions.functions),
...Object.keys(arrayExtensions.functions),
...Object.keys(objectExtensions.functions),
...Object.keys(booleanExtensions.functions),
...Object.keys(genericExtensions),
]),
);
@@ -455,7 +458,7 @@ function findExtendedFunction(input: unknown, functionName: string): FoundFuncti
let foundFunction: Function | undefined;
if (Array.isArray(input)) {
foundFunction = arrayExtensions.functions[functionName];
} else if (isDate(input) && functionName !== 'toDate') {
} else if (isDate(input) && functionName !== 'toDate' && functionName !== 'toDateTime') {
// If it's a string date (from $json), convert it to a Date object,
// unless that function is `toDate`, since `toDate` does something
// very different on date objects
@@ -469,6 +472,8 @@ function findExtendedFunction(input: unknown, functionName: string): FoundFuncti
foundFunction = dateExtensions.functions[functionName];
} else if (input !== null && typeof input === 'object') {
foundFunction = objectExtensions.functions[functionName];
} else if (typeof input === 'boolean') {
foundFunction = booleanExtensions.functions[functionName];
}
// Look for generic or builtin

View File

@@ -1,6 +1,7 @@
/**
* @jest-environment jsdom
*/
import { DateTime } from 'luxon';
import { ExpressionExtensionError } from '../errors/expression-extension.error';
import type { ExtensionMap } from './Extensions';
@@ -40,6 +41,41 @@ function round(value: number, extraArgs: number[]) {
return +value.toFixed(decimalPlaces);
}
function toBoolean(value: number) {
return value !== 0;
}
function toInt(value: number) {
return round(value, []);
}
function toFloat(value: number) {
return value;
}
type DateTimeFormat = 'ms' | 's' | 'excel';
function toDateTime(value: number, extraArgs: [DateTimeFormat]) {
const [valueFormat = 'ms'] = extraArgs;
switch (valueFormat) {
// Excel format is days since 1900
// There is a bug where 1900 is incorrectly treated as a leap year
case 'excel': {
const DAYS_BETWEEN_1900_1970 = 25567;
const DAYS_LEAP_YEAR_BUG_ADJUST = 2;
const SECONDS_IN_DAY = 86_400;
return DateTime.fromSeconds(
(value - (DAYS_BETWEEN_1900_1970 + DAYS_LEAP_YEAR_BUG_ADJUST)) * SECONDS_IN_DAY,
);
}
case 's':
return DateTime.fromSeconds(value);
case 'ms':
default:
return DateTime.fromMillis(value);
}
}
ceil.doc = {
name: 'ceil',
description: 'Rounds up a number to a whole number.',
@@ -89,6 +125,26 @@ round.doc = {
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/numbers/#number-round',
};
toBoolean.doc = {
name: 'toBoolean',
description: 'Converts a number to a boolean. 0 is `false`, all other numbers are `true`.',
section: 'cast',
returnType: 'boolean',
docURL:
'https://docs.n8n.io/code/builtin/data-transformation-functions/numbers/#number-toBoolean',
};
toDateTime.doc = {
name: 'toDateTime',
description:
"Converts a number to a DateTime. Defaults to milliseconds. Format can be 'ms' (milliseconds), 's' (seconds) or 'excel' (Excel 1900 format).",
section: 'cast',
returnType: 'DateTime',
args: [{ name: 'format?', type: 'string' }],
docURL:
'https://docs.n8n.io/code/builtin/data-transformation-functions/numbers/#number-toDateTime',
};
export const numberExtensions: ExtensionMap = {
typeName: 'Number',
functions: {
@@ -98,5 +154,9 @@ export const numberExtensions: ExtensionMap = {
round,
isEven,
isOdd,
toBoolean,
toInt,
toFloat,
toDateTime,
},
};

View File

@@ -88,6 +88,26 @@ export function urlEncode(value: object) {
return new URLSearchParams(value as Record<string, string>).toString();
}
export function toJsonString(value: object) {
return JSON.stringify(value);
}
export function toInt() {
return undefined;
}
export function toFloat() {
return undefined;
}
export function toBoolean() {
return undefined;
}
export function toDateTime() {
return undefined;
}
isEmpty.doc = {
name: 'isEmpty',
description: 'Checks if the Object has no key-value pairs.',
@@ -168,6 +188,14 @@ values.doc = {
returnType: 'Array',
};
toJsonString.doc = {
name: 'toJsonString',
description: 'Converts an object to a JSON string',
docURL:
'https://docs.n8n.io/code/builtin/data-transformation-functions/objects/#object-toJsonString',
returnType: 'string',
};
export const objectExtensions: ExtensionMap = {
typeName: 'Object',
functions: {
@@ -181,5 +209,10 @@ export const objectExtensions: ExtensionMap = {
urlEncode,
keys,
values,
toJsonString,
toInt,
toFloat,
toBoolean,
toDateTime,
},
};

View File

@@ -1,10 +1,12 @@
import SHA from 'jssha';
import MD5 from 'md5';
import { encode } from 'js-base64';
import { toBase64, fromBase64 } from 'js-base64';
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 { tryToParseDateTime } from '../TypeValidation';
export const SupportedHashAlgorithms = [
'md5',
@@ -116,7 +118,7 @@ function hash(value: string, extraArgs: string[]): string {
const algorithm = extraArgs[0]?.toLowerCase() ?? 'md5';
switch (algorithm) {
case 'base64':
return encode(value);
return toBase64(value);
case 'md5':
return MD5(value);
case 'sha1':
@@ -214,6 +216,14 @@ function toDate(value: string): Date {
return date;
}
function toDateTime(value: string): DateTime {
try {
return tryToParseDateTime(value);
} catch (error) {
throw new ExpressionExtensionError('cannot convert to Luxon DateTime');
}
}
function urlDecode(value: string, extraArgs: boolean[]): string {
const [entireString = false] = extraArgs;
if (entireString) {
@@ -359,6 +369,37 @@ function extractUrl(value: string) {
return matched[0];
}
function extractUrlPath(value: string) {
try {
const url = new URL(value);
return url.pathname;
} catch (error) {
return undefined;
}
}
function parseJson(value: string): unknown {
try {
return JSON.parse(value);
} catch (error) {
return undefined;
}
}
function toBoolean(value: string): boolean {
const normalized = value.toLowerCase();
const FALSY = new Set(['false', 'no', '0']);
return normalized.length > 0 && !FALSY.has(normalized);
}
function base64Encode(value: string): string {
return toBase64(value);
}
function base64Decode(value: string): string {
return fromBase64(value);
}
removeMarkdown.doc = {
name: 'removeMarkdown',
description: 'Removes Markdown formatting from a string.',
@@ -382,9 +423,28 @@ toDate.doc = {
description: 'Converts a string to a date.',
section: 'cast',
returnType: 'Date',
hidden: true,
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-toDate',
};
toDateTime.doc = {
name: 'toDateTime',
description: 'Converts a string to a Luxon DateTime.',
section: 'cast',
returnType: 'DateTime',
docURL:
'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-toDateTime',
};
toBoolean.doc = {
name: 'toBoolean',
description: 'Converts a string to a boolean.',
section: 'cast',
returnType: 'boolean',
docURL:
'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-toBoolean',
};
toFloat.doc = {
name: 'toFloat',
description: 'Converts a string to a decimal number.',
@@ -549,6 +609,15 @@ extractUrl.doc = {
'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-extractUrl',
};
extractUrlPath.doc = {
name: 'extractUrlPath',
description: 'Extracts the path from a URL. Returns undefined if none is found.',
section: 'edit',
returnType: 'string',
docURL:
'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-extractUrlPath',
};
hash.doc = {
name: 'hash',
description: 'Returns a string hashed with the given algorithm. Default algorithm is `md5`.',
@@ -567,6 +636,34 @@ quote.doc = {
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-quote',
};
parseJson.doc = {
name: 'parseJson',
description:
'Parses a JSON string, constructing the JavaScript value or object described by the string.',
section: 'cast',
returnType: 'any',
docURL:
'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-parseJson',
};
base64Encode.doc = {
name: 'base64Encode',
description: 'Converts a UTF-8-encoded string to a Base64 string.',
section: 'edit',
returnType: 'string',
docURL:
'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-base64Encode',
};
base64Decode.doc = {
name: 'base64Decode',
description: 'Converts a Base64 string to a UTF-8 string.',
section: 'edit',
returnType: 'string',
docURL:
'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-base64Decode',
};
const toDecimalNumber: Extension = toFloat.bind({});
toDecimalNumber.doc = { ...toFloat.doc, hidden: true };
const toWholeNumber: Extension = toInt.bind({});
@@ -579,6 +676,8 @@ export const stringExtensions: ExtensionMap = {
removeMarkdown,
removeTags,
toDate,
toDateTime,
toBoolean,
toDecimalNumber,
toFloat,
toInt,
@@ -600,5 +699,9 @@ export const stringExtensions: ExtensionMap = {
extractEmail,
extractDomain,
extractUrl,
extractUrlPath,
parseJson,
base64Encode,
base64Decode,
},
};