mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
fix(core): Fix date issues for data tables (no-changelog) (#18981)
This commit is contained in:
@@ -183,13 +183,17 @@ describe('dataStore filters', () => {
|
||||
columns: [
|
||||
{ name: 'name', type: 'string' },
|
||||
{ name: 'age', type: 'number' },
|
||||
{ name: 'birthday', type: 'date' },
|
||||
{ name: 'isActive', type: 'boolean' },
|
||||
],
|
||||
});
|
||||
|
||||
const maryBirthday = new Date('1998-08-25');
|
||||
|
||||
const rows = [
|
||||
{ name: 'John', age: 30 },
|
||||
{ name: 'Mary', age: 25 },
|
||||
{ name: 'Jack', age: 35 },
|
||||
{ name: 'John', age: 30, birthday: new Date('1994-05-15T00:00:00.000Z'), isActive: true },
|
||||
{ name: 'Mary', age: 25, birthday: maryBirthday, isActive: false },
|
||||
{ name: 'Jack', age: 35, birthday: new Date('1988-12-05T00:00:00.000Z'), isActive: true },
|
||||
];
|
||||
|
||||
await dataStoreService.insertRows(dataStoreId, project.id, rows);
|
||||
@@ -198,13 +202,25 @@ describe('dataStore filters', () => {
|
||||
const result = await dataStoreService.getManyRowsAndCount(dataStoreId, project.id, {
|
||||
filter: {
|
||||
type: 'and',
|
||||
filters: [{ columnName: 'name', value: 'Mary', condition: 'eq' }],
|
||||
filters: [
|
||||
{ columnName: 'name', value: 'Mary', condition: 'eq' },
|
||||
{ columnName: 'age', value: 25, condition: 'eq' },
|
||||
{ columnName: 'birthday', value: maryBirthday, condition: 'eq' },
|
||||
{ columnName: 'isActive', value: false, condition: 'eq' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
expect(result.count).toEqual(1);
|
||||
expect(result.data).toEqual([expect.objectContaining({ name: 'Mary', age: 25 })]);
|
||||
expect(result.data).toEqual([
|
||||
expect.objectContaining({
|
||||
name: 'Mary',
|
||||
age: 25,
|
||||
birthday: maryBirthday,
|
||||
isActive: false,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("retrieves rows with 'not equals' filter correctly", async () => {
|
||||
|
||||
@@ -47,6 +47,7 @@ function getConditionAndParams(
|
||||
filter: ListDataStoreContentFilter['filters'][number],
|
||||
index: number,
|
||||
dbType: DataSourceOptions['type'],
|
||||
columns?: DataTableColumn[],
|
||||
): [string, Record<string, unknown>] {
|
||||
const paramName = `filter_${index}`;
|
||||
const column = `${quoteIdentifier('dataStore', dbType)}.${quoteIdentifier(filter.columnName, dbType)}`;
|
||||
@@ -60,6 +61,10 @@ function getConditionAndParams(
|
||||
}
|
||||
}
|
||||
|
||||
// Find the column type to normalize the value consistently
|
||||
const columnInfo = columns?.find((col) => col.name === filter.columnName);
|
||||
const value = columnInfo ? normalizeValue(filter.value, columnInfo?.type, dbType) : filter.value;
|
||||
|
||||
// Handle operators that map directly to SQL operators
|
||||
const operators: Record<string, string> = {
|
||||
eq: '=',
|
||||
@@ -71,38 +76,35 @@ function getConditionAndParams(
|
||||
};
|
||||
|
||||
if (operators[filter.condition]) {
|
||||
return [
|
||||
`${column} ${operators[filter.condition]} :${paramName}`,
|
||||
{ [paramName]: filter.value },
|
||||
];
|
||||
return [`${column} ${operators[filter.condition]} :${paramName}`, { [paramName]: value }];
|
||||
}
|
||||
|
||||
switch (filter.condition) {
|
||||
// case-sensitive
|
||||
case 'like':
|
||||
if (['sqlite', 'sqlite-pooled'].includes(dbType)) {
|
||||
const globValue = toSqliteGlobFromPercent(filter.value as string);
|
||||
const globValue = toSqliteGlobFromPercent(value as string);
|
||||
return [`${column} GLOB :${paramName}`, { [paramName]: globValue }];
|
||||
}
|
||||
|
||||
if (['mysql', 'mariadb'].includes(dbType)) {
|
||||
const escapedValue = escapeLikeSpecials(filter.value as string);
|
||||
const escapedValue = escapeLikeSpecials(value as string);
|
||||
return [`${column} LIKE BINARY :${paramName} ESCAPE '\\\\'`, { [paramName]: escapedValue }];
|
||||
}
|
||||
|
||||
// PostgreSQL: LIKE is case-sensitive
|
||||
if (dbType === 'postgres') {
|
||||
const escapedValue = escapeLikeSpecials(filter.value as string);
|
||||
const escapedValue = escapeLikeSpecials(value as string);
|
||||
return [`${column} LIKE :${paramName} ESCAPE '\\'`, { [paramName]: escapedValue }];
|
||||
}
|
||||
|
||||
// Generic fallback
|
||||
return [`${column} LIKE :${paramName}`, { [paramName]: filter.value }];
|
||||
return [`${column} LIKE :${paramName}`, { [paramName]: value }];
|
||||
|
||||
// case-insensitive
|
||||
case 'ilike':
|
||||
if (['sqlite', 'sqlite-pooled'].includes(dbType)) {
|
||||
const escapedValue = escapeLikeSpecials(filter.value as string);
|
||||
const escapedValue = escapeLikeSpecials(value as string);
|
||||
return [
|
||||
`UPPER(${column}) LIKE UPPER(:${paramName}) ESCAPE '\\'`,
|
||||
{ [paramName]: escapedValue },
|
||||
@@ -110,7 +112,7 @@ function getConditionAndParams(
|
||||
}
|
||||
|
||||
if (['mysql', 'mariadb'].includes(dbType)) {
|
||||
const escapedValue = escapeLikeSpecials(filter.value as string);
|
||||
const escapedValue = escapeLikeSpecials(value as string);
|
||||
return [
|
||||
`UPPER(${column}) LIKE UPPER(:${paramName}) ESCAPE '\\\\'`,
|
||||
{ [paramName]: escapedValue },
|
||||
@@ -118,11 +120,11 @@ function getConditionAndParams(
|
||||
}
|
||||
|
||||
if (dbType === 'postgres') {
|
||||
const escapedValue = escapeLikeSpecials(filter.value as string);
|
||||
const escapedValue = escapeLikeSpecials(value as string);
|
||||
return [`${column} ILIKE :${paramName} ESCAPE '\\'`, { [paramName]: escapedValue }];
|
||||
}
|
||||
|
||||
return [`UPPER(${column}) LIKE UPPER(:${paramName})`, { [paramName]: filter.value }];
|
||||
return [`UPPER(${column}) LIKE UPPER(:${paramName})`, { [paramName]: value }];
|
||||
}
|
||||
|
||||
// This should never happen as all valid conditions are handled above
|
||||
@@ -381,8 +383,12 @@ export class DataStoreRowsRepository {
|
||||
await queryRunner.query(deleteColumnQuery(this.toTableName(dataStoreId), columnName, dbType));
|
||||
}
|
||||
|
||||
async getManyAndCount(dataStoreId: string, dto: ListDataStoreContentQueryDto) {
|
||||
const [countQuery, query] = this.getManyQuery(dataStoreId, dto);
|
||||
async getManyAndCount(
|
||||
dataStoreId: string,
|
||||
dto: ListDataStoreContentQueryDto,
|
||||
columns?: DataTableColumn[],
|
||||
) {
|
||||
const [countQuery, query] = this.getManyQuery(dataStoreId, dto, columns);
|
||||
const data: DataStoreRowsReturn = await query.select('*').getRawMany();
|
||||
const countResult = await countQuery.select('COUNT(*) as count').getRawOne<{
|
||||
count: number | string | null;
|
||||
@@ -423,11 +429,12 @@ export class DataStoreRowsRepository {
|
||||
private getManyQuery(
|
||||
dataStoreId: string,
|
||||
dto: ListDataStoreContentQueryDto,
|
||||
columns?: DataTableColumn[],
|
||||
): [QueryBuilder, QueryBuilder] {
|
||||
const query = this.dataSource.createQueryBuilder();
|
||||
|
||||
query.from(this.toTableName(dataStoreId), 'dataStore');
|
||||
this.applyFilters(query, dto);
|
||||
this.applyFilters(query, dto, columns);
|
||||
const countQuery = query.clone().select('COUNT(*)');
|
||||
this.applySorting(query, dto);
|
||||
this.applyPagination(query, dto);
|
||||
@@ -435,13 +442,17 @@ export class DataStoreRowsRepository {
|
||||
return [countQuery, query];
|
||||
}
|
||||
|
||||
private applyFilters(query: QueryBuilder, dto: ListDataStoreContentQueryDto): void {
|
||||
private applyFilters(
|
||||
query: QueryBuilder,
|
||||
dto: ListDataStoreContentQueryDto,
|
||||
columns?: DataTableColumn[],
|
||||
): void {
|
||||
const filters = dto.filter?.filters ?? [];
|
||||
const filterType = dto.filter?.type ?? 'and';
|
||||
|
||||
const dbType = this.dataSource.options.type;
|
||||
const conditionsAndParams = filters.map((filter, i) =>
|
||||
getConditionAndParams(filter, i, dbType),
|
||||
getConditionAndParams(filter, i, dbType, columns),
|
||||
);
|
||||
|
||||
for (const [condition, params] of conditionsAndParams) {
|
||||
|
||||
@@ -113,7 +113,7 @@ export class DataStoreService {
|
||||
// a renamed/removed column appearing here (or added column missing) if the store was
|
||||
// modified between when the frontend sent the request and we received it
|
||||
const columns = await this.dataStoreColumnRepository.getColumns(dataStoreId);
|
||||
const result = await this.dataStoreRowsRepository.getManyAndCount(dataStoreId, dto);
|
||||
const result = await this.dataStoreRowsRepository.getManyAndCount(dataStoreId, dto, columns);
|
||||
return {
|
||||
count: result.count,
|
||||
data: normalizeRows(result.data, columns),
|
||||
|
||||
@@ -259,21 +259,34 @@ export function normalizeRows(rows: DataStoreRowsReturn, columns: DataTableColum
|
||||
});
|
||||
}
|
||||
|
||||
function formatDateForDatabase(date: Date, dbType?: DataSourceOptions['type']): string {
|
||||
// MySQL/MariaDB DATETIME format doesn't accept ISO strings with 'Z' timezone
|
||||
if (dbType === 'mysql' || dbType === 'mariadb') {
|
||||
return date.toISOString().replace('T', ' ').replace('Z', '');
|
||||
}
|
||||
// PostgreSQL and SQLite accept ISO strings
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
export function normalizeValue(
|
||||
value: DataStoreColumnJsType,
|
||||
columnType: string | undefined,
|
||||
dbType: DataSourceOptions['type'],
|
||||
dbType?: DataSourceOptions['type'],
|
||||
): DataStoreColumnJsType {
|
||||
if (['mysql', 'mariadb'].includes(dbType)) {
|
||||
if (columnType === 'date') {
|
||||
if (value instanceof Date) {
|
||||
return value;
|
||||
} else if (typeof value === 'string') {
|
||||
const date = new Date(value);
|
||||
if (!isNaN(date.getTime())) {
|
||||
return date;
|
||||
}
|
||||
}
|
||||
if (columnType !== 'date' || value === null || value === undefined) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Convert Date objects to appropriate string format for database parameter binding
|
||||
if (value instanceof Date) {
|
||||
return formatDateForDatabase(value, dbType);
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const date = new Date(value);
|
||||
if (!isNaN(date.getTime())) {
|
||||
// Convert parsed date strings to appropriate format
|
||||
return formatDateForDatabase(date, dbType);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user