diff --git a/packages/cli/src/modules/data-table/__tests__/data-store-filters.test.ts b/packages/cli/src/modules/data-table/__tests__/data-store-filters.test.ts index aeb26793a4..8c4c5524c8 100644 --- a/packages/cli/src/modules/data-table/__tests__/data-store-filters.test.ts +++ b/packages/cli/src/modules/data-table/__tests__/data-store-filters.test.ts @@ -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 () => { diff --git a/packages/cli/src/modules/data-table/data-store-rows.repository.ts b/packages/cli/src/modules/data-table/data-store-rows.repository.ts index f9ab249b52..5a4e0e6093 100644 --- a/packages/cli/src/modules/data-table/data-store-rows.repository.ts +++ b/packages/cli/src/modules/data-table/data-store-rows.repository.ts @@ -47,6 +47,7 @@ function getConditionAndParams( filter: ListDataStoreContentFilter['filters'][number], index: number, dbType: DataSourceOptions['type'], + columns?: DataTableColumn[], ): [string, Record] { 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 = { 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) { diff --git a/packages/cli/src/modules/data-table/data-store.service.ts b/packages/cli/src/modules/data-table/data-store.service.ts index 01127be5a7..d20bcd3657 100644 --- a/packages/cli/src/modules/data-table/data-store.service.ts +++ b/packages/cli/src/modules/data-table/data-store.service.ts @@ -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), diff --git a/packages/cli/src/modules/data-table/utils/sql-utils.ts b/packages/cli/src/modules/data-table/utils/sql-utils.ts index a706ce0b5a..2ec5eb689b 100644 --- a/packages/cli/src/modules/data-table/utils/sql-utils.ts +++ b/packages/cli/src/modules/data-table/utils/sql-utils.ts @@ -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); } }