diff --git a/packages/nodes-base/nodes/DataTable/common/constants.ts b/packages/nodes-base/nodes/DataTable/common/constants.ts index cf40858db1..c40a0e36b2 100644 --- a/packages/nodes-base/nodes/DataTable/common/constants.ts +++ b/packages/nodes-base/nodes/DataTable/common/constants.ts @@ -5,8 +5,13 @@ export const ALL_FILTERS = 'allFilters'; export type FilterType = typeof ANY_FILTER | typeof ALL_FILTERS; -export type FieldEntry = { - keyName: string; - condition: 'eq' | 'neq' | 'like' | 'ilike' | 'gt' | 'gte' | 'lt' | 'lte'; - keyValue: DataStoreColumnJsType; -}; +export type FieldEntry = + | { + keyName: string; + condition: 'isEmpty' | 'isNotEmpty'; + } + | { + keyName: string; + condition: 'eq' | 'neq' | 'like' | 'ilike' | 'gt' | 'gte' | 'lt' | 'lte'; + keyValue: DataStoreColumnJsType; + }; diff --git a/packages/nodes-base/nodes/DataTable/common/methods.ts b/packages/nodes-base/nodes/DataTable/common/methods.ts index 41ac717ff7..c2fa87cef3 100644 --- a/packages/nodes-base/nodes/DataTable/common/methods.ts +++ b/packages/nodes-base/nodes/DataTable/common/methods.ts @@ -75,6 +75,8 @@ export async function getConditionsForColumn(this: ILoadOptionsFunctions) { const baseConditions: INodePropertyOptions[] = [ { name: 'Equals', value: 'eq' }, { name: 'Not Equals', value: 'neq' }, + { name: 'Is Empty', value: 'isEmpty' }, + { name: 'Is Not Empty', value: 'isNotEmpty' }, ]; const comparableConditions: INodePropertyOptions[] = [ diff --git a/packages/nodes-base/nodes/DataTable/common/selectMany.ts b/packages/nodes-base/nodes/DataTable/common/selectMany.ts index 773cc47007..3be3704128 100644 --- a/packages/nodes-base/nodes/DataTable/common/selectMany.ts +++ b/packages/nodes-base/nodes/DataTable/common/selectMany.ts @@ -78,6 +78,11 @@ export function getSelectFields( name: 'keyValue', type: 'string', default: '', + displayOptions: { + hide: { + condition: ['isEmpty', 'isNotEmpty'], + }, + }, }, ], }, @@ -89,13 +94,14 @@ export function getSelectFields( export function getSelectFilter(ctx: IExecuteFunctions, index: number) { const fields = ctx.getNodeParameter('filters.conditions', index, []); - const matchType = ctx.getNodeParameter('matchType', index, []); + const matchType = ctx.getNodeParameter('matchType', index, 'anyFilter'); + const node = ctx.getNode(); if (!isMatchType(matchType)) { - throw new NodeOperationError(ctx.getNode(), 'unexpected match type'); + throw new NodeOperationError(node, 'unexpected match type'); } if (!isFieldArray(fields)) { - throw new NodeOperationError(ctx.getNode(), 'unexpected fields input'); + throw new NodeOperationError(node, 'unexpected fields input'); } return buildGetManyFilter(fields, matchType); diff --git a/packages/nodes-base/nodes/DataTable/common/utils.ts b/packages/nodes-base/nodes/DataTable/common/utils.ts index ba47f8c0c7..86df618329 100644 --- a/packages/nodes-base/nodes/DataTable/common/utils.ts +++ b/packages/nodes-base/nodes/DataTable/common/utils.ts @@ -72,7 +72,7 @@ export async function getDataTableAggregateProxy( export function isFieldEntry(obj: unknown): obj is FieldEntry { if (obj === null || typeof obj !== 'object') return false; - return 'keyName' in obj && 'condition' in obj && 'keyValue' in obj; + return 'keyName' in obj && 'condition' in obj; // keyValue is optional } export function isMatchType(obj: unknown): obj is FilterType { @@ -83,11 +83,28 @@ export function buildGetManyFilter( fieldEntries: FieldEntry[], matchType: FilterType, ): ListDataStoreContentFilter { - const filters = fieldEntries.map((x) => ({ - columnName: x.keyName, - condition: x.condition, - value: x.keyValue, - })); + const filters = fieldEntries.map((x) => { + switch (x.condition) { + case 'isEmpty': + return { + columnName: x.keyName, + condition: 'eq' as const, + value: null, + }; + case 'isNotEmpty': + return { + columnName: x.keyName, + condition: 'neq' as const, + value: null, + }; + default: + return { + columnName: x.keyName, + condition: x.condition, + value: x.keyValue, + }; + } + }); return { type: matchType === 'allFilters' ? 'and' : 'or', filters }; } diff --git a/packages/nodes-base/nodes/DataTable/test/common/utils.test.ts b/packages/nodes-base/nodes/DataTable/test/common/utils.test.ts index 46fca52681..adfb7dbd78 100644 --- a/packages/nodes-base/nodes/DataTable/test/common/utils.test.ts +++ b/packages/nodes-base/nodes/DataTable/test/common/utils.test.ts @@ -2,18 +2,18 @@ import { DateTime } from 'luxon'; import type { INode } from 'n8n-workflow'; import { NodeOperationError } from 'n8n-workflow'; -import { dataObjectToApiInput } from '../../common/utils'; +import { dataObjectToApiInput, buildGetManyFilter } from '../../common/utils'; + +const mockNode: INode = { + id: 'test-node', + name: 'Test Node', + type: 'test', + typeVersion: 1, + position: [0, 0], + parameters: {}, +}; describe('dataObjectToApiInput', () => { - const mockNode: INode = { - id: 'test-node', - name: 'Test Node', - type: 'test', - typeVersion: 1, - position: [0, 0], - parameters: {}, - }; - describe('primitive types', () => { it('should handle string values', () => { const input = { name: 'John', email: 'john@example.com' }; @@ -201,3 +201,97 @@ describe('dataObjectToApiInput', () => { }); }); }); + +describe('buildGetManyFilter - isEmpty/isNotEmpty translation', () => { + it('should translate isEmpty to eq with null value', () => { + const fieldEntries = [{ keyName: 'name', condition: 'isEmpty' as const, keyValue: 'ignored' }]; + + const result = buildGetManyFilter(fieldEntries, 'allFilters'); + + expect(result).toEqual({ + type: 'and', + filters: [ + { + columnName: 'name', + condition: 'eq', + value: null, + }, + ], + }); + }); + + it('should translate isNotEmpty to neq with null value', () => { + const fieldEntries = [ + { keyName: 'email', condition: 'isNotEmpty' as const, keyValue: 'ignored' }, + ]; + + const result = buildGetManyFilter(fieldEntries, 'anyFilter'); + + expect(result).toEqual({ + type: 'or', + filters: [ + { + columnName: 'email', + condition: 'neq', + value: null, + }, + ], + }); + }); + + it('should handle mixed conditions including isEmpty/isNotEmpty', () => { + const fieldEntries = [ + { keyName: 'name', condition: 'eq' as const, keyValue: 'John' }, + { keyName: 'email', condition: 'isEmpty' as const, keyValue: 'ignored' }, + { keyName: 'phone', condition: 'isNotEmpty' as const, keyValue: 'ignored' }, + ]; + + const result = buildGetManyFilter(fieldEntries, 'allFilters'); + + expect(result).toEqual({ + type: 'and', + filters: [ + { + columnName: 'name', + condition: 'eq', + value: 'John', + }, + { + columnName: 'email', + condition: 'eq', + value: null, + }, + { + columnName: 'phone', + condition: 'neq', + value: null, + }, + ], + }); + }); + + it('should preserve existing conditions unchanged', () => { + const fieldEntries = [ + { keyName: 'age', condition: 'gt' as const, keyValue: 18 }, + { keyName: 'name', condition: 'like' as const, keyValue: '%john%' }, + ]; + + const result = buildGetManyFilter(fieldEntries, 'anyFilter'); + + expect(result).toEqual({ + type: 'or', + filters: [ + { + columnName: 'age', + condition: 'gt', + value: 18, + }, + { + columnName: 'name', + condition: 'like', + value: '%john%', + }, + ], + }); + }); +});