mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +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: [
|
columns: [
|
||||||
{ name: 'name', type: 'string' },
|
{ name: 'name', type: 'string' },
|
||||||
{ name: 'age', type: 'number' },
|
{ name: 'age', type: 'number' },
|
||||||
|
{ name: 'birthday', type: 'date' },
|
||||||
|
{ name: 'isActive', type: 'boolean' },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const maryBirthday = new Date('1998-08-25');
|
||||||
|
|
||||||
const rows = [
|
const rows = [
|
||||||
{ name: 'John', age: 30 },
|
{ name: 'John', age: 30, birthday: new Date('1994-05-15T00:00:00.000Z'), isActive: true },
|
||||||
{ name: 'Mary', age: 25 },
|
{ name: 'Mary', age: 25, birthday: maryBirthday, isActive: false },
|
||||||
{ name: 'Jack', age: 35 },
|
{ name: 'Jack', age: 35, birthday: new Date('1988-12-05T00:00:00.000Z'), isActive: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
await dataStoreService.insertRows(dataStoreId, project.id, rows);
|
await dataStoreService.insertRows(dataStoreId, project.id, rows);
|
||||||
@@ -198,13 +202,25 @@ describe('dataStore filters', () => {
|
|||||||
const result = await dataStoreService.getManyRowsAndCount(dataStoreId, project.id, {
|
const result = await dataStoreService.getManyRowsAndCount(dataStoreId, project.id, {
|
||||||
filter: {
|
filter: {
|
||||||
type: 'and',
|
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
|
// ASSERT
|
||||||
expect(result.count).toEqual(1);
|
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 () => {
|
it("retrieves rows with 'not equals' filter correctly", async () => {
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ function getConditionAndParams(
|
|||||||
filter: ListDataStoreContentFilter['filters'][number],
|
filter: ListDataStoreContentFilter['filters'][number],
|
||||||
index: number,
|
index: number,
|
||||||
dbType: DataSourceOptions['type'],
|
dbType: DataSourceOptions['type'],
|
||||||
|
columns?: DataTableColumn[],
|
||||||
): [string, Record<string, unknown>] {
|
): [string, Record<string, unknown>] {
|
||||||
const paramName = `filter_${index}`;
|
const paramName = `filter_${index}`;
|
||||||
const column = `${quoteIdentifier('dataStore', dbType)}.${quoteIdentifier(filter.columnName, dbType)}`;
|
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
|
// Handle operators that map directly to SQL operators
|
||||||
const operators: Record<string, string> = {
|
const operators: Record<string, string> = {
|
||||||
eq: '=',
|
eq: '=',
|
||||||
@@ -71,38 +76,35 @@ function getConditionAndParams(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (operators[filter.condition]) {
|
if (operators[filter.condition]) {
|
||||||
return [
|
return [`${column} ${operators[filter.condition]} :${paramName}`, { [paramName]: value }];
|
||||||
`${column} ${operators[filter.condition]} :${paramName}`,
|
|
||||||
{ [paramName]: filter.value },
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (filter.condition) {
|
switch (filter.condition) {
|
||||||
// case-sensitive
|
// case-sensitive
|
||||||
case 'like':
|
case 'like':
|
||||||
if (['sqlite', 'sqlite-pooled'].includes(dbType)) {
|
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 }];
|
return [`${column} GLOB :${paramName}`, { [paramName]: globValue }];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['mysql', 'mariadb'].includes(dbType)) {
|
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 }];
|
return [`${column} LIKE BINARY :${paramName} ESCAPE '\\\\'`, { [paramName]: escapedValue }];
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostgreSQL: LIKE is case-sensitive
|
// PostgreSQL: LIKE is case-sensitive
|
||||||
if (dbType === 'postgres') {
|
if (dbType === 'postgres') {
|
||||||
const escapedValue = escapeLikeSpecials(filter.value as string);
|
const escapedValue = escapeLikeSpecials(value as string);
|
||||||
return [`${column} LIKE :${paramName} ESCAPE '\\'`, { [paramName]: escapedValue }];
|
return [`${column} LIKE :${paramName} ESCAPE '\\'`, { [paramName]: escapedValue }];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic fallback
|
// Generic fallback
|
||||||
return [`${column} LIKE :${paramName}`, { [paramName]: filter.value }];
|
return [`${column} LIKE :${paramName}`, { [paramName]: value }];
|
||||||
|
|
||||||
// case-insensitive
|
// case-insensitive
|
||||||
case 'ilike':
|
case 'ilike':
|
||||||
if (['sqlite', 'sqlite-pooled'].includes(dbType)) {
|
if (['sqlite', 'sqlite-pooled'].includes(dbType)) {
|
||||||
const escapedValue = escapeLikeSpecials(filter.value as string);
|
const escapedValue = escapeLikeSpecials(value as string);
|
||||||
return [
|
return [
|
||||||
`UPPER(${column}) LIKE UPPER(:${paramName}) ESCAPE '\\'`,
|
`UPPER(${column}) LIKE UPPER(:${paramName}) ESCAPE '\\'`,
|
||||||
{ [paramName]: escapedValue },
|
{ [paramName]: escapedValue },
|
||||||
@@ -110,7 +112,7 @@ function getConditionAndParams(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (['mysql', 'mariadb'].includes(dbType)) {
|
if (['mysql', 'mariadb'].includes(dbType)) {
|
||||||
const escapedValue = escapeLikeSpecials(filter.value as string);
|
const escapedValue = escapeLikeSpecials(value as string);
|
||||||
return [
|
return [
|
||||||
`UPPER(${column}) LIKE UPPER(:${paramName}) ESCAPE '\\\\'`,
|
`UPPER(${column}) LIKE UPPER(:${paramName}) ESCAPE '\\\\'`,
|
||||||
{ [paramName]: escapedValue },
|
{ [paramName]: escapedValue },
|
||||||
@@ -118,11 +120,11 @@ function getConditionAndParams(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dbType === 'postgres') {
|
if (dbType === 'postgres') {
|
||||||
const escapedValue = escapeLikeSpecials(filter.value as string);
|
const escapedValue = escapeLikeSpecials(value as string);
|
||||||
return [`${column} ILIKE :${paramName} ESCAPE '\\'`, { [paramName]: escapedValue }];
|
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
|
// 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));
|
await queryRunner.query(deleteColumnQuery(this.toTableName(dataStoreId), columnName, dbType));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getManyAndCount(dataStoreId: string, dto: ListDataStoreContentQueryDto) {
|
async getManyAndCount(
|
||||||
const [countQuery, query] = this.getManyQuery(dataStoreId, dto);
|
dataStoreId: string,
|
||||||
|
dto: ListDataStoreContentQueryDto,
|
||||||
|
columns?: DataTableColumn[],
|
||||||
|
) {
|
||||||
|
const [countQuery, query] = this.getManyQuery(dataStoreId, dto, columns);
|
||||||
const data: DataStoreRowsReturn = await query.select('*').getRawMany();
|
const data: DataStoreRowsReturn = await query.select('*').getRawMany();
|
||||||
const countResult = await countQuery.select('COUNT(*) as count').getRawOne<{
|
const countResult = await countQuery.select('COUNT(*) as count').getRawOne<{
|
||||||
count: number | string | null;
|
count: number | string | null;
|
||||||
@@ -423,11 +429,12 @@ export class DataStoreRowsRepository {
|
|||||||
private getManyQuery(
|
private getManyQuery(
|
||||||
dataStoreId: string,
|
dataStoreId: string,
|
||||||
dto: ListDataStoreContentQueryDto,
|
dto: ListDataStoreContentQueryDto,
|
||||||
|
columns?: DataTableColumn[],
|
||||||
): [QueryBuilder, QueryBuilder] {
|
): [QueryBuilder, QueryBuilder] {
|
||||||
const query = this.dataSource.createQueryBuilder();
|
const query = this.dataSource.createQueryBuilder();
|
||||||
|
|
||||||
query.from(this.toTableName(dataStoreId), 'dataStore');
|
query.from(this.toTableName(dataStoreId), 'dataStore');
|
||||||
this.applyFilters(query, dto);
|
this.applyFilters(query, dto, columns);
|
||||||
const countQuery = query.clone().select('COUNT(*)');
|
const countQuery = query.clone().select('COUNT(*)');
|
||||||
this.applySorting(query, dto);
|
this.applySorting(query, dto);
|
||||||
this.applyPagination(query, dto);
|
this.applyPagination(query, dto);
|
||||||
@@ -435,13 +442,17 @@ export class DataStoreRowsRepository {
|
|||||||
return [countQuery, query];
|
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 filters = dto.filter?.filters ?? [];
|
||||||
const filterType = dto.filter?.type ?? 'and';
|
const filterType = dto.filter?.type ?? 'and';
|
||||||
|
|
||||||
const dbType = this.dataSource.options.type;
|
const dbType = this.dataSource.options.type;
|
||||||
const conditionsAndParams = filters.map((filter, i) =>
|
const conditionsAndParams = filters.map((filter, i) =>
|
||||||
getConditionAndParams(filter, i, dbType),
|
getConditionAndParams(filter, i, dbType, columns),
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const [condition, params] of conditionsAndParams) {
|
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
|
// 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
|
// modified between when the frontend sent the request and we received it
|
||||||
const columns = await this.dataStoreColumnRepository.getColumns(dataStoreId);
|
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 {
|
return {
|
||||||
count: result.count,
|
count: result.count,
|
||||||
data: normalizeRows(result.data, columns),
|
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(
|
export function normalizeValue(
|
||||||
value: DataStoreColumnJsType,
|
value: DataStoreColumnJsType,
|
||||||
columnType: string | undefined,
|
columnType: string | undefined,
|
||||||
dbType: DataSourceOptions['type'],
|
dbType?: DataSourceOptions['type'],
|
||||||
): DataStoreColumnJsType {
|
): DataStoreColumnJsType {
|
||||||
if (['mysql', 'mariadb'].includes(dbType)) {
|
if (columnType !== 'date' || value === null || value === undefined) {
|
||||||
if (columnType === 'date') {
|
|
||||||
if (value instanceof Date) {
|
|
||||||
return value;
|
return value;
|
||||||
} else if (typeof value === 'string') {
|
}
|
||||||
|
|
||||||
|
// 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);
|
const date = new Date(value);
|
||||||
if (!isNaN(date.getTime())) {
|
if (!isNaN(date.getTime())) {
|
||||||
return date;
|
// Convert parsed date strings to appropriate format
|
||||||
}
|
return formatDateForDatabase(date, dbType);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user