mirror of
https://github.com/apache/superset.git
synced 2026-04-20 16:44:46 +00:00
feat(ag-grid): Server Side Filtering for Column Level Filters (#35683)
This commit is contained in:
@@ -25,30 +25,46 @@ import {
|
||||
type AgGridFilterModel,
|
||||
type AgGridFilter,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
getStartOfDay,
|
||||
getEndOfDay,
|
||||
FILTER_OPERATORS,
|
||||
SQL_OPERATORS,
|
||||
validateColumnName,
|
||||
} from './utils/agGridFilterConverter';
|
||||
|
||||
/**
|
||||
* AG Grid text filter type to backend operator mapping
|
||||
* Maps custom server-side date filter operators to normalized operator names.
|
||||
* Server-side operators (serverEquals, serverBefore, etc.) are custom operators
|
||||
* used when server_pagination is enabled to bypass client-side filtering.
|
||||
*/
|
||||
const TEXT_FILTER_OPERATORS: Record<string, string> = {
|
||||
equals: '==',
|
||||
notEqual: '!=',
|
||||
contains: 'ILIKE',
|
||||
notContains: 'NOT ILIKE',
|
||||
startsWith: 'ILIKE',
|
||||
endsWith: 'ILIKE',
|
||||
const DATE_FILTER_OPERATOR_MAP: Record<string, string> = {
|
||||
// Standard operators
|
||||
[FILTER_OPERATORS.EQUALS]: FILTER_OPERATORS.EQUALS,
|
||||
[FILTER_OPERATORS.NOT_EQUAL]: FILTER_OPERATORS.NOT_EQUAL,
|
||||
[FILTER_OPERATORS.LESS_THAN]: FILTER_OPERATORS.LESS_THAN,
|
||||
[FILTER_OPERATORS.LESS_THAN_OR_EQUAL]: FILTER_OPERATORS.LESS_THAN_OR_EQUAL,
|
||||
[FILTER_OPERATORS.GREATER_THAN]: FILTER_OPERATORS.GREATER_THAN,
|
||||
[FILTER_OPERATORS.GREATER_THAN_OR_EQUAL]:
|
||||
FILTER_OPERATORS.GREATER_THAN_OR_EQUAL,
|
||||
[FILTER_OPERATORS.IN_RANGE]: FILTER_OPERATORS.IN_RANGE,
|
||||
// Custom server-side operators (map to standard equivalents)
|
||||
[FILTER_OPERATORS.SERVER_EQUALS]: FILTER_OPERATORS.EQUALS,
|
||||
[FILTER_OPERATORS.SERVER_NOT_EQUAL]: FILTER_OPERATORS.NOT_EQUAL,
|
||||
[FILTER_OPERATORS.SERVER_BEFORE]: FILTER_OPERATORS.LESS_THAN,
|
||||
[FILTER_OPERATORS.SERVER_AFTER]: FILTER_OPERATORS.GREATER_THAN,
|
||||
[FILTER_OPERATORS.SERVER_IN_RANGE]: FILTER_OPERATORS.IN_RANGE,
|
||||
};
|
||||
|
||||
/**
|
||||
* AG Grid number filter type to backend operator mapping
|
||||
* Blank filter operator types
|
||||
*/
|
||||
const NUMBER_FILTER_OPERATORS: Record<string, string> = {
|
||||
equals: '==',
|
||||
notEqual: '!=',
|
||||
lessThan: '<',
|
||||
lessThanOrEqual: '<=',
|
||||
greaterThan: '>',
|
||||
greaterThanOrEqual: '>=',
|
||||
};
|
||||
const BLANK_OPERATORS: Set<string> = new Set([
|
||||
FILTER_OPERATORS.BLANK,
|
||||
FILTER_OPERATORS.NOT_BLANK,
|
||||
FILTER_OPERATORS.SERVER_BLANK,
|
||||
FILTER_OPERATORS.SERVER_NOT_BLANK,
|
||||
]);
|
||||
|
||||
/** Escapes single quotes in SQL strings: O'Hara → O''Hara */
|
||||
function escapeStringValue(value: string): string {
|
||||
@@ -56,18 +72,77 @@ function escapeStringValue(value: string): string {
|
||||
}
|
||||
|
||||
function getTextComparator(type: string, value: string): string {
|
||||
if (type === 'contains' || type === 'notContains') {
|
||||
if (
|
||||
type === FILTER_OPERATORS.CONTAINS ||
|
||||
type === FILTER_OPERATORS.NOT_CONTAINS
|
||||
) {
|
||||
return `%${value}%`;
|
||||
}
|
||||
if (type === 'startsWith') {
|
||||
if (type === FILTER_OPERATORS.STARTS_WITH) {
|
||||
return `${value}%`;
|
||||
}
|
||||
if (type === 'endsWith') {
|
||||
if (type === FILTER_OPERATORS.ENDS_WITH) {
|
||||
return `%${value}`;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a date filter to SQL clause.
|
||||
* Handles both standard operators (equals, lessThan, etc.) and
|
||||
* custom server-side operators (serverEquals, serverBefore, etc.).
|
||||
*
|
||||
* @param colId - Column identifier
|
||||
* @param filter - AG Grid date filter object
|
||||
* @returns SQL clause string or null if conversion not possible
|
||||
*/
|
||||
function convertDateFilterToSQL(
|
||||
colId: string,
|
||||
filter: AgGridFilter,
|
||||
): string | null {
|
||||
const { type, dateFrom, dateTo } = filter;
|
||||
|
||||
if (!type) return null;
|
||||
|
||||
// Map custom server operators to standard ones
|
||||
const normalizedType = DATE_FILTER_OPERATOR_MAP[type] || type;
|
||||
|
||||
switch (normalizedType) {
|
||||
case FILTER_OPERATORS.EQUALS:
|
||||
if (!dateFrom) return null;
|
||||
// Full day range for equals
|
||||
return `(${colId} >= '${getStartOfDay(dateFrom)}' AND ${colId} <= '${getEndOfDay(dateFrom)}')`;
|
||||
|
||||
case FILTER_OPERATORS.NOT_EQUAL:
|
||||
if (!dateFrom) return null;
|
||||
// Outside the full day range for not equals
|
||||
return `(${colId} < '${getStartOfDay(dateFrom)}' OR ${colId} > '${getEndOfDay(dateFrom)}')`;
|
||||
|
||||
case FILTER_OPERATORS.LESS_THAN:
|
||||
if (!dateFrom) return null;
|
||||
return `${colId} < '${getStartOfDay(dateFrom)}'`;
|
||||
|
||||
case FILTER_OPERATORS.LESS_THAN_OR_EQUAL:
|
||||
if (!dateFrom) return null;
|
||||
return `${colId} <= '${getEndOfDay(dateFrom)}'`;
|
||||
|
||||
case FILTER_OPERATORS.GREATER_THAN:
|
||||
if (!dateFrom) return null;
|
||||
return `${colId} > '${getEndOfDay(dateFrom)}'`;
|
||||
|
||||
case FILTER_OPERATORS.GREATER_THAN_OR_EQUAL:
|
||||
if (!dateFrom) return null;
|
||||
return `${colId} >= '${getStartOfDay(dateFrom)}'`;
|
||||
|
||||
case FILTER_OPERATORS.IN_RANGE:
|
||||
if (!dateFrom || !dateTo) return null;
|
||||
return `${colId} BETWEEN '${getStartOfDay(dateFrom)}' AND '${getEndOfDay(dateTo)}'`;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts AG Grid sortModel to backend sortBy format
|
||||
*/
|
||||
@@ -111,11 +186,18 @@ export function convertColumnState(
|
||||
* - Complex: {operator: 'AND', condition1: {type: 'greaterThan', filter: 1}, condition2: {type: 'lessThan', filter: 16}}
|
||||
* → "(column_name > 1 AND column_name < 16)"
|
||||
* - Set: {filterType: 'set', values: ['a', 'b']} → "column_name IN ('a', 'b')"
|
||||
* - Blank: {filterType: 'text', type: 'blank'} → "column_name IS NULL"
|
||||
* - Date: {filterType: 'date', type: 'serverBefore', dateFrom: '2024-01-01'} → "column_name < '2024-01-01T00:00:00'"
|
||||
*/
|
||||
function convertFilterToSQL(
|
||||
colId: string,
|
||||
filter: AgGridFilter,
|
||||
): string | null {
|
||||
// Validate column name to prevent SQL injection and malformed queries
|
||||
if (!validateColumnName(colId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Complex filter: has operator and conditions
|
||||
if (
|
||||
filter.operator &&
|
||||
@@ -137,14 +219,43 @@ function convertFilterToSQL(
|
||||
return `(${conditions.join(` ${filter.operator} `)})`;
|
||||
}
|
||||
|
||||
// Handle blank/notBlank operators for all filter types
|
||||
// These are special operators that check for NULL values
|
||||
if (filter.type && BLANK_OPERATORS.has(filter.type)) {
|
||||
if (
|
||||
filter.type === FILTER_OPERATORS.BLANK ||
|
||||
filter.type === FILTER_OPERATORS.SERVER_BLANK
|
||||
) {
|
||||
return `${colId} ${SQL_OPERATORS.IS_NULL}`;
|
||||
}
|
||||
if (
|
||||
filter.type === FILTER_OPERATORS.NOT_BLANK ||
|
||||
filter.type === FILTER_OPERATORS.SERVER_NOT_BLANK
|
||||
) {
|
||||
return `${colId} ${SQL_OPERATORS.IS_NOT_NULL}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (filter.filterType === 'text' && filter.filter && filter.type) {
|
||||
const op = TEXT_FILTER_OPERATORS[filter.type];
|
||||
const escapedFilter = escapeStringValue(String(filter.filter));
|
||||
const val = getTextComparator(filter.type, escapedFilter);
|
||||
|
||||
return op === 'ILIKE' || op === 'NOT ILIKE'
|
||||
? `${colId} ${op} '${val}'`
|
||||
: `${colId} ${op} '${escapedFilter}'`;
|
||||
// Map text filter types to SQL operators
|
||||
switch (filter.type) {
|
||||
case FILTER_OPERATORS.EQUALS:
|
||||
return `${colId} ${SQL_OPERATORS.EQUALS} '${escapedFilter}'`;
|
||||
case FILTER_OPERATORS.NOT_EQUAL:
|
||||
return `${colId} ${SQL_OPERATORS.NOT_EQUALS} '${escapedFilter}'`;
|
||||
case FILTER_OPERATORS.CONTAINS:
|
||||
return `${colId} ${SQL_OPERATORS.ILIKE} '${val}'`;
|
||||
case FILTER_OPERATORS.NOT_CONTAINS:
|
||||
return `${colId} ${SQL_OPERATORS.NOT_ILIKE} '${val}'`;
|
||||
case FILTER_OPERATORS.STARTS_WITH:
|
||||
case FILTER_OPERATORS.ENDS_WITH:
|
||||
return `${colId} ${SQL_OPERATORS.ILIKE} '${val}'`;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -152,14 +263,28 @@ function convertFilterToSQL(
|
||||
filter.filter !== undefined &&
|
||||
filter.type
|
||||
) {
|
||||
const op = NUMBER_FILTER_OPERATORS[filter.type];
|
||||
return `${colId} ${op} ${filter.filter}`;
|
||||
// Map number filter types to SQL operators
|
||||
switch (filter.type) {
|
||||
case FILTER_OPERATORS.EQUALS:
|
||||
return `${colId} ${SQL_OPERATORS.EQUALS} ${filter.filter}`;
|
||||
case FILTER_OPERATORS.NOT_EQUAL:
|
||||
return `${colId} ${SQL_OPERATORS.NOT_EQUALS} ${filter.filter}`;
|
||||
case FILTER_OPERATORS.LESS_THAN:
|
||||
return `${colId} ${SQL_OPERATORS.LESS_THAN} ${filter.filter}`;
|
||||
case FILTER_OPERATORS.LESS_THAN_OR_EQUAL:
|
||||
return `${colId} ${SQL_OPERATORS.LESS_THAN_OR_EQUAL} ${filter.filter}`;
|
||||
case FILTER_OPERATORS.GREATER_THAN:
|
||||
return `${colId} ${SQL_OPERATORS.GREATER_THAN} ${filter.filter}`;
|
||||
case FILTER_OPERATORS.GREATER_THAN_OR_EQUAL:
|
||||
return `${colId} ${SQL_OPERATORS.GREATER_THAN_OR_EQUAL} ${filter.filter}`;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (filter.filterType === 'date' && filter.dateFrom && filter.type) {
|
||||
const op = NUMBER_FILTER_OPERATORS[filter.type];
|
||||
const escapedDate = escapeStringValue(filter.dateFrom);
|
||||
return `${colId} ${op} '${escapedDate}'`;
|
||||
// Handle date filters with proper date formatting and custom server operators
|
||||
if (filter.filterType === 'date' && filter.type) {
|
||||
return convertDateFilterToSQL(colId, filter);
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user