From 1c7225ebdb1d92ce45313bbab27b0839d963fc4c Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Mon, 13 Nov 2023 13:28:41 +0200 Subject: [PATCH] feat(core): Expression function $ifEmpty (#7660) Github issue / Community forum post (link here to close automatically): --------- Co-authored-by: Elias Meire --- .../completions/base.completions.ts | 6 +++- packages/editor-ui/src/constants.ts | 1 + .../completions/dollar.completions.ts | 2 +- packages/editor-ui/src/plugins/i18n/index.ts | 1 + .../src/plugins/i18n/locales/en.json | 1 + .../src/Extensions/ExtendedFunctions.ts | 33 ++++++++++++++++++- .../ExpressionExtension.test.ts | 13 ++++++++ 7 files changed, 54 insertions(+), 3 deletions(-) diff --git a/packages/editor-ui/src/components/CodeNodeEditor/completions/base.completions.ts b/packages/editor-ui/src/components/CodeNodeEditor/completions/base.completions.ts index be79c9eb6e..9ed0ddea8e 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/completions/base.completions.ts +++ b/packages/editor-ui/src/components/CodeNodeEditor/completions/base.completions.ts @@ -44,7 +44,7 @@ export const baseCompletions = defineComponent({ /** * - 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. */ baseCompletions(context: CompletionContext): CompletionResult | null { @@ -58,6 +58,10 @@ export const baseCompletions = defineComponent({ label: `${prefix}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}prevNode`, diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index 055a9ab544..8640f01f58 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -460,6 +460,7 @@ export const MAPPING_PARAMS = [ '$env', '$evaluateExpression', '$execution', + '$ifEmpty', '$input', '$item', '$jmespath', diff --git a/packages/editor-ui/src/plugins/codemirror/completions/dollar.completions.ts b/packages/editor-ui/src/plugins/codemirror/completions/dollar.completions.ts index 4ef653f56e..2ff7c97468 100644 --- a/packages/editor-ui/src/plugins/codemirror/completions/dollar.completions.ts +++ b/packages/editor-ui/src/plugins/codemirror/completions/dollar.completions.ts @@ -47,7 +47,7 @@ export function dollarCompletions(context: CompletionContext): CompletionResult export function dollarOptions() { const rank = setRank(['$json', '$input']); const SKIP = new Set(); - const DOLLAR_FUNCTIONS = ['$jmespath']; + const DOLLAR_FUNCTIONS = ['$jmespath', '$ifEmpty']; if (isCredentialsModalOpen()) { return useExternalSecretsStore().isEnterpriseExternalSecretsEnabled diff --git a/packages/editor-ui/src/plugins/i18n/index.ts b/packages/editor-ui/src/plugins/i18n/index.ts index 05581014ee..faa86c72c8 100644 --- a/packages/editor-ui/src/plugins/i18n/index.ts +++ b/packages/editor-ui/src/plugins/i18n/index.ts @@ -328,6 +328,7 @@ export class I18nClass { rootVars: Record = { $binary: this.baseText('codeNodeEditor.completer.binary'), $execution: this.baseText('codeNodeEditor.completer.$execution'), + $ifEmpty: this.baseText('codeNodeEditor.completer.$ifEmpty'), $input: this.baseText('codeNodeEditor.completer.$input'), $jmespath: this.baseText('codeNodeEditor.completer.$jmespath'), $json: this.baseText('codeNodeEditor.completer.json'), diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index e318436d5e..76bc87b9ab 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -168,6 +168,7 @@ "codeNodeEditor.completer.$execution.customData.get()": "Get custom data set in the current execution. Learn More", "codeNodeEditor.completer.$execution.customData.setAll()": "Set multiple custom data key/value pairs with an object for the current execution. Learn More", "codeNodeEditor.completer.$execution.customData.getAll()": "Get all custom data for the current execution. Learn More", + "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 node’s input data", "codeNodeEditor.completer.$input.all": "@:_reusableBaseText.codeNodeEditor.completer.all", "codeNodeEditor.completer.$input.first": "@:_reusableBaseText.codeNodeEditor.completer.first", diff --git a/packages/workflow/src/Extensions/ExtendedFunctions.ts b/packages/workflow/src/Extensions/ExtendedFunctions.ts index 73fb21ca90..927ea87a39 100644 --- a/packages/workflow/src/Extensions/ExtendedFunctions.ts +++ b/packages/workflow/src/Extensions/ExtendedFunctions.ts @@ -1,4 +1,4 @@ -import { ExpressionExtensionError } from '../ExpressionError'; +import { ExpressionError, ExpressionExtensionError } from '../ExpressionError'; import { average as aAverage } from './ArrayExtensions'; const min = Math.min; @@ -39,6 +39,36 @@ const not = (value: unknown): boolean => { return !value; }; +function ifEmpty(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 = { min, max, @@ -50,4 +80,5 @@ export const extendedFunctions = { $max: max, $average: average, $not: not, + $ifEmpty: ifEmpty, }; diff --git a/packages/workflow/test/ExpressionExtensions/ExpressionExtension.test.ts b/packages/workflow/test/ExpressionExtensions/ExpressionExtension.test.ts index cfc825fdae..2c2f8e391a 100644 --- a/packages/workflow/test/ExpressionExtensions/ExpressionExtension.test.ts +++ b/packages/workflow/test/ExpressionExtensions/ExpressionExtension.test.ts @@ -228,5 +228,18 @@ describe('tmpl Expression Parser', () => { expect(evaluate('={{ $not("") }}')).toEqual(true); 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 }); + }); }); });