mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(Data Table Node): Add new filters to node operation conditions (no-changelog) (#18942)
Co-authored-by: Charlie Kolb <charlie@n8n.io>
This commit is contained in:
@@ -3,7 +3,12 @@ import { NodeConnectionTypes } from 'n8n-workflow';
|
||||
|
||||
import { router } from './actions/router';
|
||||
import * as row from './actions/row/Row.resource';
|
||||
import { getDataTableColumns, getDataTables, tableSearch } from './common/methods';
|
||||
import {
|
||||
getConditionsForColumn,
|
||||
getDataTableColumns,
|
||||
getDataTables,
|
||||
tableSearch,
|
||||
} from './common/methods';
|
||||
|
||||
export class DataTable implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
@@ -48,6 +53,7 @@ export class DataTable implements INodeType {
|
||||
},
|
||||
loadOptions: {
|
||||
getDataTableColumns,
|
||||
getConditionsForColumn,
|
||||
},
|
||||
resourceMapping: {
|
||||
getDataTables,
|
||||
|
||||
@@ -7,6 +7,6 @@ export type FilterType = typeof ANY_FILTER | typeof ALL_FILTERS;
|
||||
|
||||
export type FieldEntry = {
|
||||
keyName: string;
|
||||
condition: 'eq' | 'neq';
|
||||
condition: 'eq' | 'neq' | 'like' | 'ilike' | 'gt' | 'gte' | 'lt' | 'lte';
|
||||
keyValue: DataStoreColumnJsType;
|
||||
};
|
||||
|
||||
@@ -42,19 +42,95 @@ export async function tableSearch(
|
||||
}
|
||||
|
||||
export async function getDataTableColumns(this: ILoadOptionsFunctions) {
|
||||
const returnData: Array<INodePropertyOptions & { type: string }> = [
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased-id, n8n-nodes-base/node-param-display-name-miscased
|
||||
const returnData: INodePropertyOptions[] = [{ name: 'id - (number)', value: 'id' }];
|
||||
{ name: 'id - (number)', value: 'id', type: 'number' },
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
{ name: 'createdAt - (date)', value: 'createdAt', type: 'date' },
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
{ name: 'updatedAt - (date)', value: 'updatedAt', type: 'date' },
|
||||
];
|
||||
const proxy = await getDataTableProxyLoadOptions(this);
|
||||
const columns = await proxy.getColumns();
|
||||
for (const column of columns) {
|
||||
returnData.push({
|
||||
name: `${column.name} - (${column.type})`,
|
||||
value: column.name,
|
||||
type: column.type,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
}
|
||||
|
||||
const systemColumns = [
|
||||
{ name: 'id', type: 'number' },
|
||||
{ name: 'createdAt', type: 'date' },
|
||||
{ name: 'updatedAt', type: 'date' },
|
||||
] as const;
|
||||
|
||||
export async function getConditionsForColumn(this: ILoadOptionsFunctions) {
|
||||
const keyName = this.getCurrentNodeParameter('&keyName') as string;
|
||||
|
||||
// Base conditions available for all column types
|
||||
const baseConditions: INodePropertyOptions[] = [
|
||||
{ name: 'Equals', value: 'eq' },
|
||||
{ name: 'Not Equals', value: 'neq' },
|
||||
];
|
||||
|
||||
const comparableConditions: INodePropertyOptions[] = [
|
||||
{ name: 'Greater Than', value: 'gt' },
|
||||
{ name: 'Greater Than or Equal', value: 'gte' },
|
||||
{ name: 'Less Than', value: 'lt' },
|
||||
{ name: 'Less Than or Equal', value: 'lte' },
|
||||
];
|
||||
|
||||
const stringConditions: INodePropertyOptions[] = [
|
||||
{
|
||||
name: 'LIKE operator',
|
||||
value: 'like',
|
||||
description:
|
||||
'Case-sensitive pattern matching. Use % as wildcard (e.g., "%Mar%" to match "Anne-Marie").',
|
||||
},
|
||||
{
|
||||
name: 'ILIKE operator',
|
||||
value: 'ilike',
|
||||
description:
|
||||
'Case-insensitive pattern matching. Use % as wildcard (e.g., "%mar%" to match "Anne-Marie").',
|
||||
},
|
||||
];
|
||||
|
||||
const allConditions = [...baseConditions, ...comparableConditions, ...stringConditions];
|
||||
|
||||
// If no column is selected yet, return all conditions
|
||||
if (!keyName) {
|
||||
return allConditions;
|
||||
}
|
||||
|
||||
// Get column type to determine available conditions
|
||||
const column =
|
||||
systemColumns.find((col) => col.name === keyName) ??
|
||||
(await (await getDataTableProxyLoadOptions(this)).getColumns()).find(
|
||||
(col) => col.name === keyName,
|
||||
);
|
||||
|
||||
if (!column) {
|
||||
return baseConditions;
|
||||
}
|
||||
|
||||
const conditions = baseConditions;
|
||||
|
||||
// String columns get LIKE operators
|
||||
if (column.type === 'string') {
|
||||
conditions.push.apply(conditions, stringConditions);
|
||||
}
|
||||
|
||||
if (['number', 'date', 'string'].includes(column.type)) {
|
||||
conditions.push.apply(conditions, comparableConditions);
|
||||
}
|
||||
|
||||
return conditions;
|
||||
}
|
||||
|
||||
export async function getDataTables(this: ILoadOptionsFunctions): Promise<ResourceMapperFields> {
|
||||
const proxy = await getDataTableProxyLoadOptions(this);
|
||||
const result = await proxy.getColumns();
|
||||
|
||||
@@ -62,19 +62,15 @@ export function getSelectFields(
|
||||
default: 'id',
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Condition',
|
||||
name: 'condition',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-missing-from-dynamic-options
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Equals',
|
||||
value: 'eq',
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['&keyName'],
|
||||
loadOptionsMethod: 'getConditionsForColumn',
|
||||
},
|
||||
{
|
||||
name: 'Not Equals',
|
||||
value: 'neq',
|
||||
},
|
||||
],
|
||||
default: 'eq',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import type { FieldEntry } from '../../common/constants';
|
||||
import { ANY_FILTER } from '../../common/constants';
|
||||
import { ANY_FILTER, ALL_FILTERS } from '../../common/constants';
|
||||
import { DATA_TABLE_ID_FIELD } from '../../common/fields';
|
||||
import { executeSelectMany } from '../../common/selectMany';
|
||||
|
||||
@@ -56,6 +56,7 @@ describe('selectMany utils', () => {
|
||||
// ASSERT
|
||||
expect(result).toEqual([{ json: { id: 1 } }]);
|
||||
});
|
||||
|
||||
it('should get a paginated amount of rows', async () => {
|
||||
// ARRANGE
|
||||
getManyRowsAndCount.mockReturnValueOnce({
|
||||
@@ -82,6 +83,7 @@ describe('selectMany utils', () => {
|
||||
expect(result[0]).toEqual({ json: { id: 0 } });
|
||||
expect(result[2344]).toEqual({ json: { id: 2344 } });
|
||||
});
|
||||
|
||||
it('should pass null through correctly', async () => {
|
||||
// ARRANGE
|
||||
getManyRowsAndCount.mockReturnValue({ data: [{ id: 1, colA: null }], count: 1 });
|
||||
@@ -92,6 +94,7 @@ describe('selectMany utils', () => {
|
||||
// ASSERT
|
||||
expect(result).toEqual([{ json: { id: 1, colA: null } }]);
|
||||
});
|
||||
|
||||
it('should panic if pagination gets out of sync', async () => {
|
||||
// ARRANGE
|
||||
getManyRowsAndCount.mockReturnValueOnce({
|
||||
@@ -113,5 +116,217 @@ describe('selectMany utils', () => {
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
describe('filter conditions', () => {
|
||||
it('should handle "eq" condition', async () => {
|
||||
// ARRANGE
|
||||
filters = [{ condition: 'eq', keyName: 'name', keyValue: 'John' }];
|
||||
getManyRowsAndCount.mockReturnValue({ data: [{ id: 1, name: 'John' }], count: 1 });
|
||||
|
||||
// ACT
|
||||
const result = await executeSelectMany(mockExecuteFunctions, 0, dataStoreProxy);
|
||||
|
||||
// ASSERT
|
||||
expect(result).toEqual([{ json: { id: 1, name: 'John' } }]);
|
||||
});
|
||||
|
||||
it('should handle "neq" condition', async () => {
|
||||
// ARRANGE
|
||||
filters = [{ condition: 'neq', keyName: 'name', keyValue: 'John' }];
|
||||
getManyRowsAndCount.mockReturnValue({ data: [{ id: 1, name: 'Jane' }], count: 1 });
|
||||
|
||||
// ACT
|
||||
const result = await executeSelectMany(mockExecuteFunctions, 0, dataStoreProxy);
|
||||
|
||||
// ASSERT
|
||||
expect(result).toEqual([{ json: { id: 1, name: 'Jane' } }]);
|
||||
});
|
||||
|
||||
it('should handle "gt" condition with numbers', async () => {
|
||||
// ARRANGE
|
||||
filters = [{ condition: 'gt', keyName: 'age', keyValue: 25 }];
|
||||
getManyRowsAndCount.mockReturnValue({ data: [{ id: 1, age: 30 }], count: 1 });
|
||||
|
||||
// ACT
|
||||
const result = await executeSelectMany(mockExecuteFunctions, 0, dataStoreProxy);
|
||||
|
||||
// ASSERT
|
||||
expect(result).toEqual([{ json: { id: 1, age: 30 } }]);
|
||||
});
|
||||
|
||||
it('should handle "gte" condition with numbers', async () => {
|
||||
// ARRANGE
|
||||
filters = [{ condition: 'gte', keyName: 'age', keyValue: 25 }];
|
||||
getManyRowsAndCount.mockReturnValue({
|
||||
data: [
|
||||
{ id: 1, age: 25 },
|
||||
{ id: 2, age: 30 },
|
||||
],
|
||||
count: 2,
|
||||
});
|
||||
|
||||
// ACT
|
||||
const result = await executeSelectMany(mockExecuteFunctions, 0, dataStoreProxy);
|
||||
|
||||
// ASSERT
|
||||
expect(result).toEqual([{ json: { id: 1, age: 25 } }, { json: { id: 2, age: 30 } }]);
|
||||
});
|
||||
|
||||
it('should handle "lt" condition with numbers', async () => {
|
||||
// ARRANGE
|
||||
filters = [{ condition: 'lt', keyName: 'age', keyValue: 30 }];
|
||||
getManyRowsAndCount.mockReturnValue({ data: [{ id: 1, age: 25 }], count: 1 });
|
||||
|
||||
// ACT
|
||||
const result = await executeSelectMany(mockExecuteFunctions, 0, dataStoreProxy);
|
||||
|
||||
// ASSERT
|
||||
expect(result).toEqual([{ json: { id: 1, age: 25 } }]);
|
||||
});
|
||||
|
||||
it('should handle "lte" condition with numbers', async () => {
|
||||
// ARRANGE
|
||||
filters = [{ condition: 'lte', keyName: 'age', keyValue: 30 }];
|
||||
getManyRowsAndCount.mockReturnValue({
|
||||
data: [
|
||||
{ id: 1, age: 25 },
|
||||
{ id: 2, age: 30 },
|
||||
],
|
||||
count: 2,
|
||||
});
|
||||
|
||||
// ACT
|
||||
const result = await executeSelectMany(mockExecuteFunctions, 0, dataStoreProxy);
|
||||
|
||||
// ASSERT
|
||||
expect(result).toEqual([{ json: { id: 1, age: 25 } }, { json: { id: 2, age: 30 } }]);
|
||||
});
|
||||
|
||||
it('should handle "like" condition with pattern matching', async () => {
|
||||
// ARRANGE
|
||||
filters = [{ condition: 'like', keyName: 'name', keyValue: '%Mar%' }];
|
||||
getManyRowsAndCount.mockReturnValue({ data: [{ id: 1, name: 'Anne-Marie' }], count: 1 });
|
||||
|
||||
// ACT
|
||||
const result = await executeSelectMany(mockExecuteFunctions, 0, dataStoreProxy);
|
||||
|
||||
// ASSERT
|
||||
expect(result).toEqual([{ json: { id: 1, name: 'Anne-Marie' } }]);
|
||||
});
|
||||
|
||||
it('should handle "ilike" condition with case-insensitive pattern matching', async () => {
|
||||
// ARRANGE
|
||||
filters = [{ condition: 'ilike', keyName: 'name', keyValue: '%mar%' }];
|
||||
getManyRowsAndCount.mockReturnValue({ data: [{ id: 1, name: 'Anne-Marie' }], count: 1 });
|
||||
|
||||
// ACT
|
||||
const result = await executeSelectMany(mockExecuteFunctions, 0, dataStoreProxy);
|
||||
|
||||
// ASSERT
|
||||
expect(result).toEqual([{ json: { id: 1, name: 'Anne-Marie' } }]);
|
||||
});
|
||||
|
||||
it('should handle multiple conditions with ANY_FILTER (OR logic - matches records satisfying either condition)', async () => {
|
||||
// ARRANGE
|
||||
filters = [
|
||||
{ condition: 'eq', keyName: 'status', keyValue: 'active' },
|
||||
{ condition: 'gt', keyName: 'age', keyValue: 50 },
|
||||
];
|
||||
getManyRowsAndCount.mockReturnValue({
|
||||
data: [{ id: 1, status: 'active', age: 25 }],
|
||||
count: 1,
|
||||
});
|
||||
|
||||
// ACT
|
||||
const result = await executeSelectMany(mockExecuteFunctions, 0, dataStoreProxy);
|
||||
|
||||
// ASSERT
|
||||
expect(result).toEqual([{ json: { id: 1, status: 'active', age: 25 } }]);
|
||||
});
|
||||
|
||||
it('should handle multiple conditions with ALL_FILTERS (AND logic - matches records satisfying all conditions)', async () => {
|
||||
// ARRANGE
|
||||
filters = [
|
||||
{ condition: 'eq', keyName: 'status', keyValue: 'active' },
|
||||
{ condition: 'gte', keyName: 'age', keyValue: 21 },
|
||||
];
|
||||
mockExecuteFunctions.getNodeParameter = jest.fn().mockImplementation((field) => {
|
||||
switch (field) {
|
||||
case DATA_TABLE_ID_FIELD:
|
||||
return dataTableId;
|
||||
case 'filters.conditions':
|
||||
return filters;
|
||||
case 'matchType':
|
||||
return ALL_FILTERS;
|
||||
}
|
||||
});
|
||||
getManyRowsAndCount.mockReturnValue({
|
||||
data: [{ id: 1, status: 'active', age: 25 }],
|
||||
count: 1,
|
||||
});
|
||||
|
||||
// ACT
|
||||
const result = await executeSelectMany(mockExecuteFunctions, 0, dataStoreProxy);
|
||||
|
||||
// ASSERT
|
||||
expect(result).toEqual([{ json: { id: 1, status: 'active', age: 25 } }]);
|
||||
});
|
||||
|
||||
it('should handle ALL_FILTERS excluding records that match only one condition (proves AND logic)', async () => {
|
||||
// ARRANGE
|
||||
filters = [
|
||||
{ condition: 'eq', keyName: 'status', keyValue: 'inactive' },
|
||||
{ condition: 'gte', keyName: 'age', keyValue: 21 },
|
||||
];
|
||||
mockExecuteFunctions.getNodeParameter = jest.fn().mockImplementation((field) => {
|
||||
switch (field) {
|
||||
case DATA_TABLE_ID_FIELD:
|
||||
return dataTableId;
|
||||
case 'filters.conditions':
|
||||
return filters;
|
||||
case 'matchType':
|
||||
return ALL_FILTERS;
|
||||
}
|
||||
});
|
||||
getManyRowsAndCount.mockReturnValue({
|
||||
data: [],
|
||||
count: 0,
|
||||
});
|
||||
|
||||
// ACT
|
||||
const result = await executeSelectMany(mockExecuteFunctions, 0, dataStoreProxy);
|
||||
|
||||
// ASSERT
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle ANY_FILTER including records that match only one condition (proves OR logic)', async () => {
|
||||
// ARRANGE
|
||||
filters = [
|
||||
{ condition: 'eq', keyName: 'status', keyValue: 'inactive' },
|
||||
{ condition: 'gte', keyName: 'age', keyValue: 21 },
|
||||
];
|
||||
mockExecuteFunctions.getNodeParameter = jest.fn().mockImplementation((field) => {
|
||||
switch (field) {
|
||||
case DATA_TABLE_ID_FIELD:
|
||||
return dataTableId;
|
||||
case 'filters.conditions':
|
||||
return filters;
|
||||
case 'matchType':
|
||||
return ANY_FILTER;
|
||||
}
|
||||
});
|
||||
getManyRowsAndCount.mockReturnValue({
|
||||
data: [{ id: 1, status: 'active', age: 25 }],
|
||||
count: 1,
|
||||
});
|
||||
|
||||
// ACT
|
||||
const result = await executeSelectMany(mockExecuteFunctions, 0, dataStoreProxy);
|
||||
|
||||
// ASSERT
|
||||
expect(result).toEqual([{ json: { id: 1, status: 'active', age: 25 } }]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,7 +45,7 @@ export type ListDataStoreContentFilter = {
|
||||
type: 'and' | 'or';
|
||||
filters: Array<{
|
||||
columnName: string;
|
||||
condition: 'eq' | 'neq';
|
||||
condition: 'eq' | 'neq' | 'like' | 'ilike' | 'gt' | 'gte' | 'lt' | 'lte';
|
||||
value: DataStoreColumnJsType;
|
||||
}>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user