feat(core): Expression function $ifEmpty (#7660)

Github issue / Community forum post (link here to close automatically):

---------

Co-authored-by: Elias Meire <elias@meire.dev>
This commit is contained in:
Michael Kret 2023-11-13 13:28:41 +02:00 committed by GitHub
parent 14035e1244
commit 1c7225ebdb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 54 additions and 3 deletions

View file

@ -44,7 +44,7 @@ export const baseCompletions = defineComponent({
/** /**
* - Complete `$` to `$execution $input $prevNode $runIndex $workflow $now $today * - Complete `$` to `$execution $input $prevNode $runIndex $workflow $now $today
* $jmespath $('nodeName')` in both modes. * $jmespath $ifEmpt $('nodeName')` in both modes.
* - Complete `$` to `$json $binary $itemIndex` in single-item mode. * - Complete `$` to `$json $binary $itemIndex` in single-item mode.
*/ */
baseCompletions(context: CompletionContext): CompletionResult | null { baseCompletions(context: CompletionContext): CompletionResult | null {
@ -58,6 +58,10 @@ export const baseCompletions = defineComponent({
label: `${prefix}execution`, label: `${prefix}execution`,
info: this.$locale.baseText('codeNodeEditor.completer.$execution'), info: this.$locale.baseText('codeNodeEditor.completer.$execution'),
}, },
{
label: `${prefix}ifEmpty()`,
info: this.$locale.baseText('codeNodeEditor.completer.$ifEmpty'),
},
{ label: `${prefix}input`, info: this.$locale.baseText('codeNodeEditor.completer.$input') }, { label: `${prefix}input`, info: this.$locale.baseText('codeNodeEditor.completer.$input') },
{ {
label: `${prefix}prevNode`, label: `${prefix}prevNode`,

View file

@ -460,6 +460,7 @@ export const MAPPING_PARAMS = [
'$env', '$env',
'$evaluateExpression', '$evaluateExpression',
'$execution', '$execution',
'$ifEmpty',
'$input', '$input',
'$item', '$item',
'$jmespath', '$jmespath',

View file

@ -47,7 +47,7 @@ export function dollarCompletions(context: CompletionContext): CompletionResult
export function dollarOptions() { export function dollarOptions() {
const rank = setRank(['$json', '$input']); const rank = setRank(['$json', '$input']);
const SKIP = new Set(); const SKIP = new Set();
const DOLLAR_FUNCTIONS = ['$jmespath']; const DOLLAR_FUNCTIONS = ['$jmespath', '$ifEmpty'];
if (isCredentialsModalOpen()) { if (isCredentialsModalOpen()) {
return useExternalSecretsStore().isEnterpriseExternalSecretsEnabled return useExternalSecretsStore().isEnterpriseExternalSecretsEnabled

View file

@ -328,6 +328,7 @@ export class I18nClass {
rootVars: Record<string, string | undefined> = { rootVars: Record<string, string | undefined> = {
$binary: this.baseText('codeNodeEditor.completer.binary'), $binary: this.baseText('codeNodeEditor.completer.binary'),
$execution: this.baseText('codeNodeEditor.completer.$execution'), $execution: this.baseText('codeNodeEditor.completer.$execution'),
$ifEmpty: this.baseText('codeNodeEditor.completer.$ifEmpty'),
$input: this.baseText('codeNodeEditor.completer.$input'), $input: this.baseText('codeNodeEditor.completer.$input'),
$jmespath: this.baseText('codeNodeEditor.completer.$jmespath'), $jmespath: this.baseText('codeNodeEditor.completer.$jmespath'),
$json: this.baseText('codeNodeEditor.completer.json'), $json: this.baseText('codeNodeEditor.completer.json'),

View file

@ -168,6 +168,7 @@
"codeNodeEditor.completer.$execution.customData.get()": "Get custom data set in the current execution. <a href=\"https://docs.n8n.io/workflows/executions/custom-executions-data/\" target=\"_blank\">Learn More</a>", "codeNodeEditor.completer.$execution.customData.get()": "Get custom data set in the current execution. <a href=\"https://docs.n8n.io/workflows/executions/custom-executions-data/\" target=\"_blank\">Learn More</a>",
"codeNodeEditor.completer.$execution.customData.setAll()": "Set multiple custom data key/value pairs with an object for the current execution. <a href=\"https://docs.n8n.io/workflows/executions/custom-executions-data/\" target=\"_blank\">Learn More</a>", "codeNodeEditor.completer.$execution.customData.setAll()": "Set multiple custom data key/value pairs with an object for the current execution. <a href=\"https://docs.n8n.io/workflows/executions/custom-executions-data/\" target=\"_blank\">Learn More</a>",
"codeNodeEditor.completer.$execution.customData.getAll()": "Get all custom data for the current execution. <a href=\"https://docs.n8n.io/workflows/executions/custom-executions-data/\" target=\"_blank\">Learn More</a>", "codeNodeEditor.completer.$execution.customData.getAll()": "Get all custom data for the current execution. <a href=\"https://docs.n8n.io/workflows/executions/custom-executions-data/\" target=\"_blank\">Learn More</a>",
"codeNodeEditor.completer.$ifEmpty": "Checks whether the first parameter is empty, and if so returns the second parameter. Otherwise returns the first parameter. The following count as empty: null/undefined values, empty strings, empty arrays, objects with no keys.",
"codeNodeEditor.completer.$input": "This nodes input data", "codeNodeEditor.completer.$input": "This nodes input data",
"codeNodeEditor.completer.$input.all": "@:_reusableBaseText.codeNodeEditor.completer.all", "codeNodeEditor.completer.$input.all": "@:_reusableBaseText.codeNodeEditor.completer.all",
"codeNodeEditor.completer.$input.first": "@:_reusableBaseText.codeNodeEditor.completer.first", "codeNodeEditor.completer.$input.first": "@:_reusableBaseText.codeNodeEditor.completer.first",

View file

@ -1,4 +1,4 @@
import { ExpressionExtensionError } from '../ExpressionError'; import { ExpressionError, ExpressionExtensionError } from '../ExpressionError';
import { average as aAverage } from './ArrayExtensions'; import { average as aAverage } from './ArrayExtensions';
const min = Math.min; const min = Math.min;
@ -39,6 +39,36 @@ const not = (value: unknown): boolean => {
return !value; return !value;
}; };
function ifEmpty<T, V>(value: V, defaultValue: T) {
if (arguments.length !== 2) {
throw new ExpressionError('expected two arguments (value, defaultValue) for this function');
}
if (value === undefined || value === null || value === '') {
return defaultValue;
}
if (typeof value === 'object') {
if (Array.isArray(value) && !value.length) {
return defaultValue;
}
if (!Object.keys(value).length) {
return defaultValue;
}
}
return value;
}
ifEmpty.doc = {
name: 'ifEmpty',
description:
'Returns the default value if the value is empty. Empty values are undefined, null, empty strings, arrays without elements and objects without keys.',
returnType: 'any',
args: [
{ name: 'value', type: 'any' },
{ name: 'defaultValue', type: 'any' },
],
docURL: 'https://docs.n8n.io/code-examples/expressions/data-transformation-functions/#if-empty',
};
export const extendedFunctions = { export const extendedFunctions = {
min, min,
max, max,
@ -50,4 +80,5 @@ export const extendedFunctions = {
$max: max, $max: max,
$average: average, $average: average,
$not: not, $not: not,
$ifEmpty: ifEmpty,
}; };

View file

@ -228,5 +228,18 @@ describe('tmpl Expression Parser', () => {
expect(evaluate('={{ $not("") }}')).toEqual(true); expect(evaluate('={{ $not("") }}')).toEqual(true);
expect(evaluate('={{ $not("a") }}')).toEqual(false); expect(evaluate('={{ $not("a") }}')).toEqual(false);
}); });
test('$ifEmpty', () => {
expect(evaluate('={{ $ifEmpty(1, "default") }}')).toEqual(1);
expect(evaluate('={{ $ifEmpty(0, "default") }}')).toEqual(0);
expect(evaluate('={{ $ifEmpty(false, "default") }}')).toEqual(false);
expect(evaluate('={{ $ifEmpty(true, "default") }}')).toEqual(true);
expect(evaluate('={{ $ifEmpty("", "default") }}')).toEqual('default');
expect(evaluate('={{ $ifEmpty(null, "default") }}')).toEqual('default');
expect(evaluate('={{ $ifEmpty(undefined, "default") }}')).toEqual('default');
expect(evaluate('={{ $ifEmpty([], "default") }}')).toEqual('default');
expect(evaluate('={{ $ifEmpty({}, "default") }}')).toEqual('default');
expect(evaluate('={{ $ifEmpty([1], "default") }}')).toEqual([1]);
expect(evaluate('={{ $ifEmpty({a: 1}, "default") }}')).toEqual({ a: 1 });
});
}); });
}); });