diff --git a/packages/editor-ui/src/components/FilterConditions/constants.ts b/packages/editor-ui/src/components/FilterConditions/constants.ts index b5b6e16c6b8726..4da26b9116f3de 100644 --- a/packages/editor-ui/src/components/FilterConditions/constants.ts +++ b/packages/editor-ui/src/components/FilterConditions/constants.ts @@ -73,6 +73,18 @@ export const OPERATORS_BY_ID = { name: 'filter.operator.notExists', singleValue: true, }, + 'number:empty': { + type: 'number', + operation: 'empty', + name: 'filter.operator.empty', + singleValue: true, + }, + 'number:notEmpty': { + type: 'number', + operation: 'notEmpty', + name: 'filter.operator.notEmpty', + singleValue: true, + }, 'number:equals': { type: 'number', operation: 'equals', name: 'filter.operator.equals' }, 'number:notEquals': { type: 'number', operation: 'notEquals', name: 'filter.operator.notEquals' }, 'number:gt': { type: 'number', operation: 'gt', name: 'filter.operator.gt' }, @@ -91,6 +103,18 @@ export const OPERATORS_BY_ID = { name: 'filter.operator.notExists', singleValue: true, }, + 'dateTime:empty': { + type: 'dateTime', + operation: 'empty', + name: 'filter.operator.empty', + singleValue: true, + }, + 'dateTime:notEmpty': { + type: 'dateTime', + operation: 'notEmpty', + name: 'filter.operator.notEmpty', + singleValue: true, + }, 'dateTime:equals': { type: 'dateTime', operation: 'equals', name: 'filter.operator.equals' }, 'dateTime:notEquals': { type: 'dateTime', @@ -121,6 +145,18 @@ export const OPERATORS_BY_ID = { name: 'filter.operator.notExists', singleValue: true, }, + 'boolean:empty': { + type: 'boolean', + operation: 'empty', + name: 'filter.operator.empty', + singleValue: true, + }, + 'boolean:notEmpty': { + type: 'boolean', + operation: 'notEmpty', + name: 'filter.operator.notEmpty', + singleValue: true, + }, 'boolean:true': { type: 'boolean', operation: 'true', diff --git a/packages/workflow/src/Extensions/ArrayExtensions.ts b/packages/workflow/src/Extensions/ArrayExtensions.ts index 5cd3166b258554..d78ec715de285b 100644 --- a/packages/workflow/src/Extensions/ArrayExtensions.ts +++ b/packages/workflow/src/Extensions/ArrayExtensions.ts @@ -352,7 +352,7 @@ compact.doc = { isEmpty.doc = { name: 'isEmpty', - description: 'Returns true if the array has no elements', + description: 'Returns true if the array has no elements or is null', examples: [ { example: '[].isEmpty()', evaluated: 'true' }, { example: "['quick', 'brown', 'fox'].isEmpty()", evaluated: 'false' }, diff --git a/packages/workflow/src/Extensions/DateExtensions.ts b/packages/workflow/src/Extensions/DateExtensions.ts index 071f6ba8507489..8c38fc35bfe1b5 100644 --- a/packages/workflow/src/Extensions/DateExtensions.ts +++ b/packages/workflow/src/Extensions/DateExtensions.ts @@ -278,6 +278,15 @@ function toBoolean() { return undefined; } +// Only null/undefined return true, this is handled in ExpressionExtension.ts +function isEmpty(): boolean { + return false; +} + +function isNotEmpty(): boolean { + return true; +} + endOfMonth.doc = { name: 'endOfMonth', returnType: 'DateTime', @@ -547,7 +556,7 @@ diffToNow.doc = { evaluated: '371.9', }, { - example: "dt = '2023-03-30T18:49:07.234.toDateTime()\ndt.diffToNow(['months', 'days'])", + example: "dt = '2023-03-30T18:49:07.234'.toDateTime()\ndt.diffToNow(['months', 'days'])", evaluated: '{ months: 12, days: 5.9 }', }, ], @@ -565,6 +574,30 @@ diffToNow.doc = { docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/dates/#date-diffToNow', }; +isEmpty.doc = { + name: 'isEmpty', + description: + 'Returns false for all DateTimes. Returns true for null.', + examples: [ + { example: "dt = '2023-03-30T18:49:07.234'.toDateTime()\ndt.isEmpty()", evaluated: 'false' }, + { example: 'dt = null\ndt.isEmpty()', evaluated: 'true' }, + ], + returnType: 'boolean', + docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/arrays/#array-isEmpty', +}; + +isNotEmpty.doc = { + name: 'isNotEmpty', + description: + 'Returns true for all DateTimes. Returns false for null.', + examples: [ + { example: "dt = '2023-03-30T18:49:07.234'.toDateTime()\ndt.isNotEmpty()", evaluated: 'true' }, + { example: 'dt = null\ndt.isNotEmpty()', evaluated: 'false' }, + ], + returnType: 'boolean', + docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/arrays/#array-isNotEmpty', +}; + export const dateExtensions: ExtensionMap = { typeName: 'Date', functions: { @@ -584,5 +617,7 @@ export const dateExtensions: ExtensionMap = { toInt, toFloat, toBoolean, + isEmpty, + isNotEmpty, }, }; diff --git a/packages/workflow/src/Extensions/ObjectExtensions.ts b/packages/workflow/src/Extensions/ObjectExtensions.ts index be85ba52d78f5c..8970318e1cab25 100644 --- a/packages/workflow/src/Extensions/ObjectExtensions.ts +++ b/packages/workflow/src/Extensions/ObjectExtensions.ts @@ -110,7 +110,8 @@ export function toDateTime() { isEmpty.doc = { name: 'isEmpty', - description: 'Returns true if the Object has no keys (fields) set', + description: + 'Returns true if the Object has no keys (fields) set or is null', examples: [ { example: "({'name': 'Nathan'}).isEmpty()", evaluated: 'false' }, { example: '({}).isEmpty()', evaluated: 'true' }, diff --git a/packages/workflow/src/Extensions/StringExtensions.ts b/packages/workflow/src/Extensions/StringExtensions.ts index 592aa8ac3c2a05..8b4e025bf678f7 100644 --- a/packages/workflow/src/Extensions/StringExtensions.ts +++ b/packages/workflow/src/Extensions/StringExtensions.ts @@ -676,7 +676,7 @@ isUrl.doc = { isEmpty.doc = { name: 'isEmpty', - description: 'Returns true if the string has no characters.', + description: 'Returns true if the string has no characters or is null', section: 'validation', returnType: 'boolean', docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-isEmpty', diff --git a/packages/workflow/src/NodeParameters/FilterParameter.ts b/packages/workflow/src/NodeParameters/FilterParameter.ts index 94a2fd2b106add..8e13f4b342eb39 100644 --- a/packages/workflow/src/NodeParameters/FilterParameter.ts +++ b/packages/workflow/src/NodeParameters/FilterParameter.ts @@ -212,6 +212,10 @@ export function executeFilterCondition( const right = rightValue as number; switch (condition.operator.operation) { + case 'empty': + return !exists; + case 'notEmpty': + return exists; case 'equals': return left === right; case 'notEquals': @@ -230,6 +234,12 @@ export function executeFilterCondition( const left = leftValue as DateTime; const right = rightValue as DateTime; + if (condition.operator.operation === 'empty') { + return !exists; + } else if (condition.operator.operation === 'notEmpty') { + return exists; + } + if (!left || !right) { return false; } @@ -254,6 +264,10 @@ export function executeFilterCondition( const right = rightValue as boolean; switch (condition.operator.operation) { + case 'empty': + return !exists; + case 'notEmpty': + return exists; case 'true': return left; case 'false': diff --git a/packages/workflow/test/FilterParameter.test.ts b/packages/workflow/test/FilterParameter.test.ts index b15f304850f94c..4e81e2e9a7b88a 100644 --- a/packages/workflow/test/FilterParameter.test.ts +++ b/packages/workflow/test/FilterParameter.test.ts @@ -570,6 +570,48 @@ describe('FilterParameter', () => { expect(result).toBe(expected); }); + it.each([ + { left: 0, expected: false }, + { left: 15, expected: false }, + { left: -15.4, expected: false }, + { left: NaN, expected: true }, + { left: null, expected: true }, + ])('number:empty($left) === $expected', ({ left, expected }) => { + const result = executeFilter( + filterFactory({ + conditions: [ + { + id: '1', + leftValue: left, + operator: { operation: 'empty', type: 'number' }, + }, + ], + }), + ); + expect(result).toBe(expected); + }); + + it.each([ + { left: 0, expected: true }, + { left: 15, expected: true }, + { left: -15.4, expected: true }, + { left: NaN, expected: false }, + { left: null, expected: false }, + ])('number:notEmpty($left) === $expected', ({ left, expected }) => { + const result = executeFilter( + filterFactory({ + conditions: [ + { + id: '1', + leftValue: left, + operator: { operation: 'notEmpty', type: 'number' }, + }, + ], + }), + ); + expect(result).toBe(expected); + }); + it.each([ { left: 0, right: 0, expected: true }, { left: 15, right: 15, expected: true }, @@ -706,6 +748,42 @@ describe('FilterParameter', () => { }); describe('dateTime', () => { + it.each([ + { left: '2023-11-15T17:10:49.113Z', expected: false }, + { left: null, expected: true }, + ])('dateTime:empty($left) === $expected', ({ left, expected }) => { + const result = executeFilter( + filterFactory({ + conditions: [ + { + id: '1', + leftValue: left, + operator: { operation: 'empty', type: 'dateTime' }, + }, + ], + }), + ); + expect(result).toBe(expected); + }); + + it.each([ + { left: '2023-11-15T17:10:49.113Z', expected: true }, + { left: null, expected: false }, + ])('dateTime:notEmpty($left) === $expected', ({ left, expected }) => { + const result = executeFilter( + filterFactory({ + conditions: [ + { + id: '1', + leftValue: left, + operator: { operation: 'notEmpty', type: 'dateTime' }, + }, + ], + }), + ); + expect(result).toBe(expected); + }); + it.each([ { left: '2023-11-15T17:10:49.113Z', right: '2023-11-15T17:10:49.113Z', expected: true }, { left: '2023-11-15T17:10:49.113Z', right: '2023-11-15T17:12:49.113Z', expected: false }, @@ -838,6 +916,44 @@ describe('FilterParameter', () => { }); describe('boolean', () => { + it.each([ + { left: true, expected: false }, + { left: false, expected: false }, + { left: null, expected: true }, + ])('boolean:empty($left) === $expected', ({ left, expected }) => { + const result = executeFilter( + filterFactory({ + conditions: [ + { + id: '1', + leftValue: left, + operator: { operation: 'empty', type: 'boolean' }, + }, + ], + }), + ); + expect(result).toBe(expected); + }); + + it.each([ + { left: true, expected: true }, + { left: false, expected: true }, + { left: null, expected: false }, + ])('boolean:notEmpty($left) === $expected', ({ left, expected }) => { + const result = executeFilter( + filterFactory({ + conditions: [ + { + id: '1', + leftValue: left, + operator: { operation: 'notEmpty', type: 'boolean' }, + }, + ], + }), + ); + expect(result).toBe(expected); + }); + it.each([ { left: true, expected: true }, { left: false, expected: false },