mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-27 14:57:21 +02:00
fix(Data Table Node): Date handling (no-changelog) (#20437)
This commit is contained in:
parent
49064fc6da
commit
f68656d6ab
|
|
@ -1121,10 +1121,10 @@ describe('dataTable filters', () => {
|
|||
dataTableId = id;
|
||||
|
||||
const rows = [
|
||||
{ name: 'Task1', registeredAt: new Date('2023-12-31') },
|
||||
{ name: 'Task2', registeredAt: new Date('2024-01-01') },
|
||||
{ name: 'Task3', registeredAt: new Date('2024-01-02') },
|
||||
{ name: 'Task4', registeredAt: new Date('2024-01-03') },
|
||||
{ name: 'Task1', registeredAt: new Date('2023-12-31T23:59:59.999Z') },
|
||||
{ name: 'Task2', registeredAt: new Date('2024-01-01T10:00:00.000Z') },
|
||||
{ name: 'Task3', registeredAt: new Date('2024-01-02T00:00:00.000Z') },
|
||||
{ name: 'Task4', registeredAt: new Date('2024-01-02T09:59:00.000Z') },
|
||||
];
|
||||
|
||||
await dataTableService.insertRows(dataTableId, project.id, rows);
|
||||
|
|
@ -1132,7 +1132,7 @@ describe('dataTable filters', () => {
|
|||
|
||||
it("retrieves rows with 'greater than' date filter correctly", async () => {
|
||||
// ACT
|
||||
const baseDate = new Date('2024-01-01');
|
||||
const baseDate = new Date('2024-01-01T12:00:00.000Z');
|
||||
const result = await dataTableService.getManyRowsAndCount(dataTableId, project.id, {
|
||||
filter: {
|
||||
type: 'and',
|
||||
|
|
@ -1166,6 +1166,59 @@ describe('dataTable filters', () => {
|
|||
expect.objectContaining({ name: 'Task3' }),
|
||||
]);
|
||||
});
|
||||
|
||||
it('filters by system createdAt column correctly', async () => {
|
||||
// ARRANGE
|
||||
const inserted = await dataTableService.insertRows(
|
||||
dataTableId,
|
||||
project.id,
|
||||
[{ name: 'TestRow' }],
|
||||
'all',
|
||||
);
|
||||
|
||||
const createdAtTimestamp = inserted[0].createdAt;
|
||||
|
||||
const midnight = new Date(createdAtTimestamp);
|
||||
midnight.setHours(0, 0, 0, 0);
|
||||
|
||||
// ACT - Check the row is not returned if filtered before midnight
|
||||
const beforeMidnightResult = await dataTableService.getManyRowsAndCount(
|
||||
dataTableId,
|
||||
project.id,
|
||||
{
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'createdAt', value: midnight, condition: 'lt' }],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// ASSERT
|
||||
expect(beforeMidnightResult.data.some((row) => row.name === 'TestRow')).toBe(false);
|
||||
|
||||
// ACT - Check the row is returned when using lte on the exact timestamp
|
||||
const result = await dataTableService.getManyRowsAndCount(dataTableId, project.id, {
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'createdAt', value: createdAtTimestamp, condition: 'lte' }],
|
||||
},
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(result.count).toBeGreaterThanOrEqual(1);
|
||||
expect(result.data.some((row) => row.name === 'TestRow')).toBe(true);
|
||||
|
||||
// ACT - - Check the row is returned when using lt on the exact timestamp
|
||||
const resultLt = await dataTableService.getManyRowsAndCount(dataTableId, project.id, {
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'createdAt', value: createdAtTimestamp, condition: 'lt' }],
|
||||
},
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(resultLt.data.some((row) => row.name === 'TestRow')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('null value validation', () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { DATA_TABLE_SYSTEM_COLUMNS, NodeOperationError } from 'n8n-workflow';
|
||||
import { DATA_TABLE_SYSTEM_COLUMN_TYPE_MAP, NodeOperationError } from 'n8n-workflow';
|
||||
import type {
|
||||
DataTableFilter,
|
||||
DataTableRowReturn,
|
||||
|
|
@ -6,6 +6,7 @@ import type {
|
|||
IDisplayOptions,
|
||||
IExecuteFunctions,
|
||||
INodeProperties,
|
||||
DataTableColumnType,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { ALL_CONDITIONS, ANY_CONDITION, ROWS_LIMIT_DEFAULT, type FilterType } from './constants';
|
||||
|
|
@ -117,15 +118,19 @@ export async function getSelectFilter(
|
|||
}
|
||||
|
||||
// Validate filter conditions against current table schema
|
||||
let allColumnsWithTypes: Record<string, DataTableColumnType> = DATA_TABLE_SYSTEM_COLUMN_TYPE_MAP;
|
||||
|
||||
if (fields.length > 0) {
|
||||
const dataTableProxy = await getDataTableProxyExecute(ctx, index);
|
||||
const availableColumns = await dataTableProxy.getColumns();
|
||||
const allColumns = new Set([
|
||||
...DATA_TABLE_SYSTEM_COLUMNS,
|
||||
...availableColumns.map((col) => col.name),
|
||||
]);
|
||||
|
||||
const invalidConditions = fields.filter((field) => !allColumns.has(field.keyName));
|
||||
// Add system columns with their types
|
||||
allColumnsWithTypes = {
|
||||
...DATA_TABLE_SYSTEM_COLUMN_TYPE_MAP,
|
||||
...Object.fromEntries(availableColumns.map((col) => [col.name, col.type])),
|
||||
};
|
||||
|
||||
const invalidConditions = fields.filter((field) => !allColumnsWithTypes[field.keyName]);
|
||||
|
||||
if (invalidConditions.length > 0) {
|
||||
const invalidColumnNames = invalidConditions.map((c) => c.keyName).join(', ');
|
||||
|
|
@ -138,7 +143,7 @@ export async function getSelectFilter(
|
|||
}
|
||||
}
|
||||
|
||||
return buildGetManyFilter(fields, matchType);
|
||||
return buildGetManyFilter(fields, matchType, allColumnsWithTypes, node);
|
||||
}
|
||||
|
||||
export async function executeSelectMany(
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type {
|
|||
IExecuteFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
DataTableColumnJsType,
|
||||
DataTableColumnType,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
|
|
@ -86,6 +87,8 @@ export function isMatchType(obj: unknown): obj is FilterType {
|
|||
export function buildGetManyFilter(
|
||||
fieldEntries: FieldEntry[],
|
||||
matchType: FilterType,
|
||||
columnTypeMap: Record<string, DataTableColumnType>,
|
||||
node: INode,
|
||||
): DataTableFilter {
|
||||
const filters = fieldEntries.map((x) => {
|
||||
switch (x.condition) {
|
||||
|
|
@ -113,12 +116,27 @@ export function buildGetManyFilter(
|
|||
condition: 'eq' as const,
|
||||
value: false,
|
||||
};
|
||||
default:
|
||||
default: {
|
||||
let value = x.keyValue;
|
||||
const columnType = columnTypeMap[x.keyName];
|
||||
|
||||
// Convert ISO date strings to Date objects for date columns
|
||||
if (columnType === 'date' && typeof value === 'string') {
|
||||
const parsed = new Date(value);
|
||||
if (isNaN(parsed.getTime())) {
|
||||
throw new NodeOperationError(
|
||||
node,
|
||||
`Invalid date string '${value}' for column '${x.keyName}'`,
|
||||
);
|
||||
}
|
||||
value = parsed;
|
||||
}
|
||||
return {
|
||||
columnName: x.keyName,
|
||||
condition: x.condition ?? 'eq',
|
||||
value: x.keyValue,
|
||||
value,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
return { type: matchType === ALL_CONDITIONS ? 'and' : 'or', filters };
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { DateTime } from 'luxon';
|
|||
import type { INode } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { ANY_CONDITION, ALL_CONDITIONS } from '../../common/constants';
|
||||
import { ANY_CONDITION, ALL_CONDITIONS, type FieldEntry } from '../../common/constants';
|
||||
import { dataObjectToApiInput, buildGetManyFilter } from '../../common/utils';
|
||||
|
||||
const mockNode: INode = {
|
||||
|
|
@ -210,7 +210,7 @@ describe('buildGetManyFilter', () => {
|
|||
{ keyName: 'name', condition: 'isEmpty' as const, keyValue: 'ignored' },
|
||||
];
|
||||
|
||||
const result = buildGetManyFilter(fieldEntries, ALL_CONDITIONS);
|
||||
const result = buildGetManyFilter(fieldEntries, ALL_CONDITIONS, { name: 'string' }, mockNode);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'and',
|
||||
|
|
@ -229,7 +229,7 @@ describe('buildGetManyFilter', () => {
|
|||
{ keyName: 'email', condition: 'isNotEmpty' as const, keyValue: 'ignored' },
|
||||
];
|
||||
|
||||
const result = buildGetManyFilter(fieldEntries, ANY_CONDITION);
|
||||
const result = buildGetManyFilter(fieldEntries, ANY_CONDITION, { email: 'string' }, mockNode);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'or',
|
||||
|
|
@ -250,7 +250,16 @@ describe('buildGetManyFilter', () => {
|
|||
{ keyName: 'phone', condition: 'isNotEmpty' as const, keyValue: 'ignored' },
|
||||
];
|
||||
|
||||
const result = buildGetManyFilter(fieldEntries, ALL_CONDITIONS);
|
||||
const result = buildGetManyFilter(
|
||||
fieldEntries,
|
||||
ALL_CONDITIONS,
|
||||
{
|
||||
name: 'string',
|
||||
email: 'string',
|
||||
phone: 'string',
|
||||
},
|
||||
mockNode,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'and',
|
||||
|
|
@ -281,7 +290,12 @@ describe('buildGetManyFilter', () => {
|
|||
{ keyName: 'isActive', condition: 'isTrue' as const, keyValue: 'ignored' },
|
||||
];
|
||||
|
||||
const result = buildGetManyFilter(fieldEntries, ALL_CONDITIONS);
|
||||
const result = buildGetManyFilter(
|
||||
fieldEntries,
|
||||
ALL_CONDITIONS,
|
||||
{ isActive: 'boolean' },
|
||||
mockNode,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'and',
|
||||
|
|
@ -300,7 +314,12 @@ describe('buildGetManyFilter', () => {
|
|||
{ keyName: 'email', condition: 'isFalse' as const, keyValue: 'ignored' },
|
||||
];
|
||||
|
||||
const result = buildGetManyFilter(fieldEntries, ANY_CONDITION);
|
||||
const result = buildGetManyFilter(
|
||||
fieldEntries,
|
||||
ANY_CONDITION,
|
||||
{ email: 'boolean' },
|
||||
mockNode,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'or',
|
||||
|
|
@ -321,7 +340,16 @@ describe('buildGetManyFilter', () => {
|
|||
{ keyName: 'isDeleted', condition: 'isFalse' as const, keyValue: 'ignored' },
|
||||
];
|
||||
|
||||
const result = buildGetManyFilter(fieldEntries, ALL_CONDITIONS);
|
||||
const result = buildGetManyFilter(
|
||||
fieldEntries,
|
||||
ALL_CONDITIONS,
|
||||
{
|
||||
name: 'string',
|
||||
isActive: 'boolean',
|
||||
isDeleted: 'boolean',
|
||||
},
|
||||
mockNode,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'and',
|
||||
|
|
@ -352,7 +380,15 @@ describe('buildGetManyFilter', () => {
|
|||
{ keyName: 'name', condition: 'like' as const, keyValue: '%john%' },
|
||||
];
|
||||
|
||||
const result = buildGetManyFilter(fieldEntries, ANY_CONDITION);
|
||||
const result = buildGetManyFilter(
|
||||
fieldEntries,
|
||||
ANY_CONDITION,
|
||||
{
|
||||
age: 'number',
|
||||
name: 'string',
|
||||
},
|
||||
mockNode,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'or',
|
||||
|
|
@ -370,4 +406,59 @@ describe('buildGetManyFilter', () => {
|
|||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe('date handling in filters', () => {
|
||||
it('should pass Date objects through unchanged', () => {
|
||||
const testDate = new Date('2025-10-06T08:14:42.274Z');
|
||||
const fieldEntries: FieldEntry[] = [
|
||||
{ keyName: 'createdAt', condition: 'lte', keyValue: testDate },
|
||||
];
|
||||
|
||||
const result = buildGetManyFilter(
|
||||
fieldEntries,
|
||||
ALL_CONDITIONS,
|
||||
{ createdAt: 'date' },
|
||||
mockNode,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'and',
|
||||
filters: [
|
||||
{
|
||||
columnName: 'createdAt',
|
||||
condition: 'lte',
|
||||
value: testDate,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert ISO date strings to Date objects', () => {
|
||||
const dateString = '2025-10-06T08:14:42.274Z';
|
||||
const fieldEntries: FieldEntry[] = [
|
||||
{ keyName: 'createdAt', condition: 'lte', keyValue: dateString },
|
||||
];
|
||||
|
||||
const result = buildGetManyFilter(
|
||||
fieldEntries,
|
||||
ALL_CONDITIONS,
|
||||
{ createdAt: 'date' },
|
||||
mockNode,
|
||||
);
|
||||
|
||||
expect(result.filters[0].value).toBeInstanceOf(Date);
|
||||
expect((result.filters[0].value as Date).toISOString()).toBe(dateString);
|
||||
});
|
||||
|
||||
it('should throw an Error for invalid date strings', () => {
|
||||
const invalidDateString = 'invalid-date';
|
||||
const fieldEntries: FieldEntry[] = [
|
||||
{ keyName: 'createdAt', condition: 'lte', keyValue: invalidDateString },
|
||||
];
|
||||
|
||||
expect(() =>
|
||||
buildGetManyFilter(fieldEntries, ALL_CONDITIONS, { createdAt: 'date' }, mockNode),
|
||||
).toThrowError(`Invalid date string '${invalidDateString}' for column 'createdAt'`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user