fix(core): Fix date issues for data tables (no-changelog) (#18981)

This commit is contained in:
Daria
2025-09-01 11:33:10 +03:00
committed by GitHub
parent b08d993529
commit 5510e4e378
4 changed files with 74 additions and 34 deletions

View File

@@ -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 () => {

View File

@@ -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) {

View File

@@ -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),

View File

@@ -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);
}
}