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 },