mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
feat(Data Table Node): Add isEmpty and isNotEmpty operations (no-changelog) (#19046)
This commit is contained in:
@@ -5,8 +5,13 @@ export const ALL_FILTERS = 'allFilters';
|
|||||||
|
|
||||||
export type FilterType = typeof ANY_FILTER | typeof ALL_FILTERS;
|
export type FilterType = typeof ANY_FILTER | typeof ALL_FILTERS;
|
||||||
|
|
||||||
export type FieldEntry = {
|
export type FieldEntry =
|
||||||
keyName: string;
|
| {
|
||||||
condition: 'eq' | 'neq' | 'like' | 'ilike' | 'gt' | 'gte' | 'lt' | 'lte';
|
keyName: string;
|
||||||
keyValue: DataStoreColumnJsType;
|
condition: 'isEmpty' | 'isNotEmpty';
|
||||||
};
|
}
|
||||||
|
| {
|
||||||
|
keyName: string;
|
||||||
|
condition: 'eq' | 'neq' | 'like' | 'ilike' | 'gt' | 'gte' | 'lt' | 'lte';
|
||||||
|
keyValue: DataStoreColumnJsType;
|
||||||
|
};
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ export async function getConditionsForColumn(this: ILoadOptionsFunctions) {
|
|||||||
const baseConditions: INodePropertyOptions[] = [
|
const baseConditions: INodePropertyOptions[] = [
|
||||||
{ name: 'Equals', value: 'eq' },
|
{ name: 'Equals', value: 'eq' },
|
||||||
{ name: 'Not Equals', value: 'neq' },
|
{ name: 'Not Equals', value: 'neq' },
|
||||||
|
{ name: 'Is Empty', value: 'isEmpty' },
|
||||||
|
{ name: 'Is Not Empty', value: 'isNotEmpty' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const comparableConditions: INodePropertyOptions[] = [
|
const comparableConditions: INodePropertyOptions[] = [
|
||||||
|
|||||||
@@ -78,6 +78,11 @@ export function getSelectFields(
|
|||||||
name: 'keyValue',
|
name: 'keyValue',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: '',
|
default: '',
|
||||||
|
displayOptions: {
|
||||||
|
hide: {
|
||||||
|
condition: ['isEmpty', 'isNotEmpty'],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -89,13 +94,14 @@ export function getSelectFields(
|
|||||||
|
|
||||||
export function getSelectFilter(ctx: IExecuteFunctions, index: number) {
|
export function getSelectFilter(ctx: IExecuteFunctions, index: number) {
|
||||||
const fields = ctx.getNodeParameter('filters.conditions', index, []);
|
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)) {
|
if (!isMatchType(matchType)) {
|
||||||
throw new NodeOperationError(ctx.getNode(), 'unexpected match type');
|
throw new NodeOperationError(node, 'unexpected match type');
|
||||||
}
|
}
|
||||||
if (!isFieldArray(fields)) {
|
if (!isFieldArray(fields)) {
|
||||||
throw new NodeOperationError(ctx.getNode(), 'unexpected fields input');
|
throw new NodeOperationError(node, 'unexpected fields input');
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildGetManyFilter(fields, matchType);
|
return buildGetManyFilter(fields, matchType);
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export async function getDataTableAggregateProxy(
|
|||||||
|
|
||||||
export function isFieldEntry(obj: unknown): obj is FieldEntry {
|
export function isFieldEntry(obj: unknown): obj is FieldEntry {
|
||||||
if (obj === null || typeof obj !== 'object') return false;
|
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 {
|
export function isMatchType(obj: unknown): obj is FilterType {
|
||||||
@@ -83,11 +83,28 @@ export function buildGetManyFilter(
|
|||||||
fieldEntries: FieldEntry[],
|
fieldEntries: FieldEntry[],
|
||||||
matchType: FilterType,
|
matchType: FilterType,
|
||||||
): ListDataStoreContentFilter {
|
): ListDataStoreContentFilter {
|
||||||
const filters = fieldEntries.map((x) => ({
|
const filters = fieldEntries.map((x) => {
|
||||||
columnName: x.keyName,
|
switch (x.condition) {
|
||||||
condition: x.condition,
|
case 'isEmpty':
|
||||||
value: x.keyValue,
|
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 };
|
return { type: matchType === 'allFilters' ? 'and' : 'or', filters };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,18 +2,18 @@ import { DateTime } from 'luxon';
|
|||||||
import type { INode } from 'n8n-workflow';
|
import type { INode } from 'n8n-workflow';
|
||||||
import { NodeOperationError } 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', () => {
|
describe('dataObjectToApiInput', () => {
|
||||||
const mockNode: INode = {
|
|
||||||
id: 'test-node',
|
|
||||||
name: 'Test Node',
|
|
||||||
type: 'test',
|
|
||||||
typeVersion: 1,
|
|
||||||
position: [0, 0],
|
|
||||||
parameters: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('primitive types', () => {
|
describe('primitive types', () => {
|
||||||
it('should handle string values', () => {
|
it('should handle string values', () => {
|
||||||
const input = { name: 'John', email: 'john@example.com' };
|
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%',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user