Compare commits

...

5 Commits

9 changed files with 1080 additions and 529 deletions

View File

@@ -0,0 +1,543 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { negateOperator, OPERATORS } from 'constants/antlrQueryConstants';
import { extractQueryPairs } from 'utils/queryContextUtils';
import {
clearQueryPairsCache,
convertFiltersToExpressionWithExistingQuery,
} from '../queryProcessor';
describe('convertFiltersToExpressionWithExistingQuery', () => {
beforeEach(() => {
clearQueryPairsCache();
jest.clearAllMocks();
});
it('should handle empty, null, and undefined inputs', () => {
// Test null and undefined existing query
expect(
convertFiltersToExpressionWithExistingQuery(null as any, undefined),
).toEqual({
filters: null,
filter: { expression: '' },
});
expect(
convertFiltersToExpressionWithExistingQuery(undefined as any, undefined),
).toEqual({
filters: undefined,
filter: { expression: '' },
});
// Test empty filters
expect(
convertFiltersToExpressionWithExistingQuery(
{ items: [], op: 'AND' },
undefined,
),
).toEqual({
filters: { items: [], op: 'AND' },
filter: { expression: '' },
});
expect(
convertFiltersToExpressionWithExistingQuery(
{ items: undefined, op: 'AND' } as any,
undefined,
),
).toEqual({
filters: { items: undefined, op: 'AND' },
filter: { expression: '' },
});
});
it('should return filters with new expression when no existing query', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'service.name', key: 'service.name', type: 'string' },
op: OPERATORS['='],
value: 'test-service',
},
],
op: 'AND',
};
const result = convertFiltersToExpressionWithExistingQuery(
filters,
undefined,
);
expect(result.filters).toEqual(filters);
expect(result.filter.expression).toBe("service.name = 'test-service'");
});
it('should handle empty filters', () => {
const filters = {
items: [],
op: 'AND',
};
const result = convertFiltersToExpressionWithExistingQuery(
filters,
undefined,
);
expect(result.filters).toEqual(filters);
expect(result.filter.expression).toBe('');
});
it('should create filters from existing query when filters array is empty', () => {
const filters = {
items: [],
op: 'AND',
};
const existingQuery = "service.name = 'testing'";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters.items).toHaveLength(1);
expect(result.filters.items[0]).toEqual({
id: expect.any(String),
key: {
id: 'service.name',
key: 'service.name',
type: '',
},
op: '=',
value: 'testing',
});
expect(result.filter.expression).toBe(existingQuery);
});
it('should create multiple filters from complex existing query', () => {
const filters = {
items: [],
op: 'AND',
};
const existingQuery =
"service.name = 'testing' AND status = 'success' AND count > 100";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters.items).toHaveLength(3);
expect(result.filters.items[0]).toEqual({
id: expect.any(String),
key: {
id: 'service.name',
key: 'service.name',
type: '',
},
op: '=',
value: 'testing',
});
expect(result.filters.items[1]).toEqual({
id: expect.any(String),
key: {
id: 'status',
key: 'status',
type: '',
},
op: '=',
value: 'success',
});
expect(result.filters.items[2]).toEqual({
id: expect.any(String),
key: {
id: 'count',
key: 'count',
type: '',
},
op: '>',
value: '100',
});
expect(result.filter.expression).toBe(existingQuery);
});
it('should handle existing query with matching filters', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'service.name', key: 'service.name', type: 'string' },
op: OPERATORS['='],
value: 'updated-service',
},
],
op: 'AND',
};
const existingQuery = "service.name = 'old-service'";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters).toBeDefined();
expect(result.filter).toBeDefined();
expect(result.filter.expression).toBe("service.name = 'updated-service'");
// Ensure parser can parse the existing query
expect(extractQueryPairs(existingQuery)).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: 'service.name',
operator: '=',
value: "'old-service'",
}),
]),
);
});
it('should handle IN operator with existing query', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'service.name', key: 'service.name', type: 'string' },
op: OPERATORS.IN,
value: ['service1', 'service2'],
},
],
op: 'AND',
};
const existingQuery = "service.name IN ['old-service']";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters).toBeDefined();
expect(result.filter).toBeDefined();
expect(result.filter.expression).toBe(
"service.name IN ['service1', 'service2']",
);
});
it('should handle IN operator conversion from equals', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'service.name', key: 'service.name', type: 'string' },
op: OPERATORS.IN,
value: ['service1', 'service2'],
},
],
op: 'AND',
};
const existingQuery = "service.name = 'old-service'";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters.items).toHaveLength(1);
expect(result.filter.expression).toBe(
"service.name IN ['service1', 'service2'] ",
);
});
it('should handle NOT IN operator conversion from not equals', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'service.name', key: 'service.name', type: 'string' },
op: negateOperator(OPERATORS.IN),
value: ['service1', 'service2'],
},
],
op: 'AND',
};
const existingQuery = "service.name != 'old-service'";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters.items).toHaveLength(1);
expect(result.filter.expression).toBe(
"service.name NOT IN ['service1', 'service2'] ",
);
});
it('should add new filters when they do not exist in existing query', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'new.key', key: 'new.key', type: 'string' },
op: OPERATORS['='],
value: 'new-value',
},
],
op: 'AND',
};
const existingQuery = "service.name = 'old-service'";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters.items).toHaveLength(2); // Original + new filter
expect(result.filter.expression).toBe(
"service.name = 'old-service' new.key = 'new-value'",
);
});
it('should handle simple value replacement', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'status', key: 'status', type: 'string' },
op: OPERATORS['='],
value: 'error',
},
],
op: 'AND',
};
const existingQuery = "status = 'success'";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters.items).toHaveLength(1);
expect(result.filter.expression).toBe("status = 'error'");
});
it('should handle filters with no key gracefully', () => {
const filters = {
items: [
{
id: '1',
key: undefined,
op: OPERATORS['='],
value: 'test-value',
},
],
op: 'AND',
};
const existingQuery = "service.name = 'old-service'";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters.items).toHaveLength(2);
expect(result.filter.expression).toBe("service.name = 'old-service'");
});
it('should normalize deprecated operators', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'service', key: 'service', type: 'string' },
op: 'regex', // deprecated operator
value: 'api',
},
],
op: 'AND',
};
const result = convertFiltersToExpressionWithExistingQuery(
filters,
undefined,
);
expect(result.filters.items[0].op).toBe('regexp');
});
it('should handle complex mixed scenarios', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'service', key: 'service', type: 'string' },
op: OPERATORS.IN,
value: ['api-gateway', 'user-service'],
},
{
id: '2',
key: { id: 'status', key: 'status', type: 'string' },
op: OPERATORS['='],
value: 'success',
},
],
op: 'AND',
};
const existingQuery = "service = 'old-service' AND count > 100";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters.items).toHaveLength(3); // 2 new + 1 existing
expect(result.filter.expression).toContain(
"service IN ['api-gateway', 'user-service']",
);
expect(result.filter.expression).toContain("status = 'success'");
expect(result.filter.expression).toContain('count > 100');
});
it('should handle empty query string', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'service', key: 'service', type: 'string' },
op: '=',
value: 'api',
},
],
op: 'AND',
};
const result = convertFiltersToExpressionWithExistingQuery(filters, '');
expect(result.filter.expression).toBe("service = 'api'");
});
it('should handle invalid query gracefully', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'service', key: 'service', type: 'string' },
op: '=',
value: 'api',
},
],
op: 'AND',
};
const result = convertFiltersToExpressionWithExistingQuery(
filters,
'invalid query',
);
expect(result.filters).toBeDefined();
expect(result.filter.expression).toBe("invalid query service = 'api'");
});
it('should preserve existing filters when no matching query pairs', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'service', key: 'service', type: 'string' },
op: '=',
value: 'api',
},
],
op: 'AND',
};
const existingQuery = "different.field = 'value'";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters.items).toHaveLength(2); // Original + existing
expect(result.filter.expression).toContain("service = 'api'");
expect(result.filter.expression).toContain("different.field = 'value'");
});
it('should handle array values in IN operators', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'services', key: 'services', type: 'string' },
op: OPERATORS.IN,
value: ['api', 'user', 'auth'],
},
],
op: 'AND',
};
const existingQuery = "services IN ['old-service']";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters.items).toHaveLength(1);
expect(result.filter.expression).toBe("services IN ['api', 'user', 'auth']");
});
it('should handle function operators', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'tags', key: 'tags', type: 'string' },
op: 'has',
value: 'production',
},
],
op: 'AND',
};
const existingQuery = '';
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters.items).toHaveLength(1);
expect(result.filter.expression).toBe("has(tags, 'production')");
});
it('should handle non-value operators like EXISTS', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'user_id', key: 'user_id', type: 'string' },
op: 'EXISTS',
value: '',
},
],
op: 'AND',
};
const existingQuery = 'user_id EXISTS';
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters.items).toHaveLength(1);
expect(result.filter.expression).toBe('user_id EXISTS');
});
});

View File

@@ -1,6 +1,5 @@
/* eslint-disable sonarjs/no-duplicate-string */
/* eslint-disable import/no-unresolved */
import { negateOperator, OPERATORS } from 'constants/antlrQueryConstants';
import {
BaseAutocompleteData,
DataTypes,
@@ -12,7 +11,6 @@ import { extractQueryPairs } from 'utils/queryContextUtils';
import {
convertAggregationToExpression,
convertFiltersToExpression,
convertFiltersToExpressionWithExistingQuery,
removeKeysFromExpression,
} from '../utils';
@@ -550,231 +548,6 @@ describe('convertFiltersToExpression', () => {
"user_id NOT EXISTS AND description NOT CONTAINS 'error' AND NOT has(tags, 'production') AND NOT hasAny(labels, ['env:prod', 'service:api'])",
});
});
it('should return filters with new expression when no existing query', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'service.name', key: 'service.name', type: 'string' },
op: OPERATORS['='],
value: 'test-service',
},
],
op: 'AND',
};
const result = convertFiltersToExpressionWithExistingQuery(
filters,
undefined,
);
expect(result.filters).toEqual(filters);
expect(result.filter.expression).toBe("service.name = 'test-service'");
});
it('should handle empty filters', () => {
const filters = {
items: [],
op: 'AND',
};
const result = convertFiltersToExpressionWithExistingQuery(
filters,
undefined,
);
expect(result.filters).toEqual(filters);
expect(result.filter.expression).toBe('');
});
it('should handle existing query with matching filters', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'service.name', key: 'service.name', type: 'string' },
op: OPERATORS['='],
value: 'updated-service',
},
],
op: 'AND',
};
const existingQuery = "service.name = 'old-service'";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters).toBeDefined();
expect(result.filter).toBeDefined();
expect(result.filter.expression).toBe("service.name = 'updated-service'");
// Ensure parser can parse the existing query
expect(extractQueryPairs(existingQuery)).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: 'service.name',
operator: '=',
value: "'old-service'",
}),
]),
);
});
it('should handle IN operator with existing query', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'service.name', key: 'service.name', type: 'string' },
op: OPERATORS.IN,
value: ['service1', 'service2'],
},
],
op: 'AND',
};
const existingQuery = "service.name IN ['old-service']";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters).toBeDefined();
expect(result.filter).toBeDefined();
expect(result.filter.expression).toBe(
"service.name IN ['service1', 'service2']",
);
});
it('should handle IN operator conversion from equals', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'service.name', key: 'service.name', type: 'string' },
op: OPERATORS.IN,
value: ['service1', 'service2'],
},
],
op: 'AND',
};
const existingQuery = "service.name = 'old-service'";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters.items).toHaveLength(1);
expect(result.filter.expression).toBe(
"service.name IN ['service1', 'service2'] ",
);
});
it('should handle NOT IN operator conversion from not equals', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'service.name', key: 'service.name', type: 'string' },
op: negateOperator(OPERATORS.IN),
value: ['service1', 'service2'],
},
],
op: 'AND',
};
const existingQuery = "service.name != 'old-service'";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters.items).toHaveLength(1);
expect(result.filter.expression).toBe(
"service.name NOT IN ['service1', 'service2'] ",
);
});
it('should add new filters when they do not exist in existing query', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'new.key', key: 'new.key', type: 'string' },
op: OPERATORS['='],
value: 'new-value',
},
],
op: 'AND',
};
const existingQuery = "service.name = 'old-service'";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters.items).toHaveLength(2); // Original + new filter
expect(result.filter.expression).toBe(
"service.name = 'old-service' new.key = 'new-value'",
);
});
it('should handle simple value replacement', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'status', key: 'status', type: 'string' },
op: OPERATORS['='],
value: 'error',
},
],
op: 'AND',
};
const existingQuery = "status = 'success'";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters.items).toHaveLength(1);
expect(result.filter.expression).toBe("status = 'error'");
});
it('should handle filters with no key gracefully', () => {
const filters = {
items: [
{
id: '1',
key: undefined,
op: OPERATORS['='],
value: 'test-value',
},
],
op: 'AND',
};
const existingQuery = "service.name = 'old-service'";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters.items).toHaveLength(2);
expect(result.filter.expression).toBe("service.name = 'old-service'");
});
});
describe('convertAggregationToExpression', () => {

View File

@@ -0,0 +1,531 @@
/* eslint-disable sonarjs/cognitive-complexity */
import {
DEPRECATED_OPERATORS_MAP,
OPERATORS,
} from 'constants/antlrQueryConstants';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { cloneDeep } from 'lodash-es';
import { IQueryPair } from 'types/antlrQueryTypes';
import {
TagFilter,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import { extractQueryPairs } from 'utils/queryContextUtils';
import { unquote } from 'utils/stringUtils';
import { v4 as uuid } from 'uuid';
import { convertFiltersToExpression } from './utils';
interface ProcessingResult {
type: 'update' | 'add' | 'skip' | 'transform';
modifications?: QueryModification[];
newFilters?: TagFilterItem[];
shouldAddToNonExisting?: boolean;
}
interface QueryModification {
type: 'replace' | 'append' | 'prepend';
startIndex?: number;
endIndex?: number;
newContent: string;
}
interface QueryProcessingContext {
readonly originalQuery: string;
queryPairsMap: Map<string, IQueryPair>;
readonly visitedPairs: Set<string>;
modifications: QueryModification[];
newFilters: TagFilterItem[];
nonExistingFilters: TagFilterItem[];
modifiedQuery: string;
}
// Cache for query pairs to avoid repeated parsing
const queryPairsCache = new Map<string, Map<string, IQueryPair>>();
// Validation functions
const validateFilter = (filter: TagFilterItem): boolean =>
!!(filter.key?.key && filter.op && filter.value !== undefined);
const validateQuery = (query: string): boolean =>
typeof query === 'string' && query.trim().length > 0;
const areValuesEqual = (existing: unknown[], current: unknown[]): boolean => {
if (existing.length !== current.length) return false;
const existingSet = new Set(existing.map((v) => String(v)));
return current.every((v) => existingSet.has(String(v)));
};
// Format a value for the expression string
const formatValueForExpression = (
value: string[] | string | number | boolean,
operator?: string,
): string => {
// Check if it's a variable
const isVariable = (val: string | string[] | number | boolean): boolean => {
if (Array.isArray(val)) {
return val.some((v) => typeof v === 'string' && v.trim().startsWith('$'));
}
return typeof val === 'string' && val.trim().startsWith('$');
};
if (isVariable(value)) {
return String(value);
}
// Check if operator requires array values
const isArrayOperator = (op: string): boolean => {
const arrayOperators = ['in', 'not in', 'IN', 'NOT IN'];
return arrayOperators.includes(op);
};
// For IN operators, ensure value is always an array
if (isArrayOperator(operator || '')) {
const arrayValue = Array.isArray(value) ? value : [value];
return `[${arrayValue
.map((v) =>
typeof v === 'string' ? `'${v.replace(/'/g, "\\'")}'` : String(v),
)
.join(', ')}]`;
}
if (Array.isArray(value)) {
// Handle array values (e.g., for IN operations)
return `[${value
.map((v) =>
typeof v === 'string' ? `'${v.replace(/'/g, "\\'")}'` : String(v),
)
.join(', ')}]`;
}
if (typeof value === 'string') {
// Add single quotes around all string values and escape internal single quotes
return `'${value.replace(/'/g, "\\'")}'`;
}
return String(value);
};
// Clear cache when needed (e.g., for testing or memory management)
export const clearQueryPairsCache = (): void => {
queryPairsCache.clear();
};
const getQueryPairsMap = (query: string): Map<string, IQueryPair> => {
const trimmedQuery = query.trim();
if (!queryPairsCache.has(trimmedQuery)) {
const queryPairs = extractQueryPairs(trimmedQuery) || [];
const queryPairsMap: Map<string, IQueryPair> = new Map();
queryPairs.forEach((pair) => {
const key = pair.hasNegation
? `${pair.key}-not ${pair.operator}`.trim().toLowerCase()
: `${pair.key}-${pair.operator}`.trim().toLowerCase();
queryPairsMap.set(key, pair);
});
queryPairsCache.set(trimmedQuery, queryPairsMap);
}
return queryPairsCache.get(trimmedQuery) || new Map();
};
// Helper function to normalize deprecated operators
const normalizeDeprecatedOperators = (filters: TagFilter): TagFilter => {
const updatedFilters = cloneDeep(filters);
if (updatedFilters?.items) {
updatedFilters.items = updatedFilters.items.map((item) => {
const opLower = item.op?.toLowerCase();
if (Object.keys(DEPRECATED_OPERATORS_MAP).includes(opLower)) {
return {
...item,
op: DEPRECATED_OPERATORS_MAP[
opLower as keyof typeof DEPRECATED_OPERATORS_MAP
].toLowerCase(),
};
}
return item;
});
}
return updatedFilters;
};
// ES5 compatible operator handlers using functions instead of classes
// Helper function to check if operator is IN or NOT IN
function isInOperator(operator: string): boolean {
const sanitizedOperator = operator.trim().toUpperCase();
return (
[OPERATORS.IN, `${OPERATORS.NOT} ${OPERATORS.IN}`].indexOf(
sanitizedOperator,
) !== -1
);
}
// Helper function to handle IN operator transformations
// Generic helper function to handle operator transformations
function handleOperatorTransformation(
filter: TagFilterItem,
context: QueryProcessingContext,
formattedValue: string,
targetOperator: string,
transformationConfigs: Array<{
operatorKey: string;
positionProperty: 'operatorStart' | 'negationStart';
}>,
): ProcessingResult {
const { key } = filter;
const { op } = filter;
// Skip if key is not defined
if (!key || !key.key) {
return { type: 'add', shouldAddToNonExisting: true };
}
// Check each transformation configuration
const foundConfig = transformationConfigs.find((config) => {
const transformationKey = `${key.key}-${config.operatorKey}`;
const transformationKeyLower = transformationKey.trim().toLowerCase();
return context.queryPairsMap.has(transformationKeyLower);
});
if (foundConfig) {
const transformationKey = `${key.key}-${foundConfig.operatorKey}`;
const transformationKeyLower = transformationKey.trim().toLowerCase();
const transformationPair = context.queryPairsMap.get(transformationKeyLower);
context.visitedPairs.add(transformationKeyLower);
if (
transformationPair &&
transformationPair.position &&
transformationPair.position.valueEnd
) {
const startPosition =
transformationPair.position[foundConfig.positionProperty];
context.modifiedQuery = `${
context.modifiedQuery.slice(0, startPosition) + targetOperator
} ${formattedValue} ${context.modifiedQuery.slice(
transformationPair.position.valueEnd + 1,
)}`;
context.queryPairsMap = getQueryPairsMap(context.modifiedQuery.trim());
}
// Mark the current filter as visited to prevent it from being added as a new filter
context.visitedPairs.add(`${key.key}-${op}`.trim().toLowerCase());
return { type: 'transform', shouldAddToNonExisting: false };
}
return { type: 'add', shouldAddToNonExisting: true };
}
function handleInTransformations(
filter: TagFilterItem,
context: QueryProcessingContext,
formattedValue: string,
): ProcessingResult {
const transformationConfigs = [
{
operatorKey: `${OPERATORS.NOT} ${filter.op}`,
positionProperty: 'negationStart' as const,
},
{ operatorKey: OPERATORS['='], positionProperty: 'operatorStart' as const },
{ operatorKey: OPERATORS['!='], positionProperty: 'operatorStart' as const },
];
return handleOperatorTransformation(
filter,
context,
formattedValue,
OPERATORS.IN,
transformationConfigs,
);
}
// Helper function to handle NOT IN operator transformations
function handleNotInTransformations(
filter: TagFilterItem,
context: QueryProcessingContext,
formattedValue: string,
): ProcessingResult {
const transformationConfigs = [
{ operatorKey: OPERATORS['!='], positionProperty: 'operatorStart' as const },
];
return handleOperatorTransformation(
filter,
context,
formattedValue,
`${OPERATORS.NOT} ${OPERATORS.IN}`,
transformationConfigs,
);
}
// Helper function to handle operator transformations
function handleOperatorTransformations(
filter: TagFilterItem,
context: QueryProcessingContext,
formattedValue: string,
): ProcessingResult {
const { op } = filter;
const sanitizedOperator = op.trim().toUpperCase();
if (sanitizedOperator === OPERATORS.IN) {
return handleInTransformations(filter, context, formattedValue);
}
if (sanitizedOperator === `${OPERATORS.NOT} ${OPERATORS.IN}`) {
return handleNotInTransformations(filter, context, formattedValue);
}
return { type: 'add', shouldAddToNonExisting: true };
}
// ES5 compatible operator handler functions
function processInOperator(
filter: TagFilterItem,
context: QueryProcessingContext,
): ProcessingResult {
const { key } = filter;
const { op } = filter;
const { value } = filter;
// Skip if key is not defined
if (!key || !key.key) {
return { type: 'skip' };
}
const formattedValue = formatValueForExpression(value, op);
const pairKey = `${key.key}-${op}`.trim().toLowerCase();
// Check if exact match exists
const existingPair = context.queryPairsMap.get(pairKey);
if (
existingPair &&
existingPair.position &&
existingPair.position.valueStart &&
existingPair.position.valueEnd
) {
context.visitedPairs.add(pairKey);
// Check if values are identical for array-based operators
if (existingPair.valueList && filter.value && Array.isArray(filter.value)) {
const cleanValues = (values: unknown[]): unknown[] =>
values.map((val) => (typeof val === 'string' ? unquote(val) : val));
const cleanExistingValues = cleanValues(existingPair.valueList);
const cleanFilterValues = cleanValues(filter.value);
if (areValuesEqual(cleanExistingValues, cleanFilterValues)) {
// Values are identical, preserve existing formatting
context.modifiedQuery =
context.modifiedQuery.slice(0, existingPair.position.valueStart) +
existingPair.value +
context.modifiedQuery.slice(existingPair.position.valueEnd + 1);
return { type: 'skip' };
}
}
// Update the value
context.modifiedQuery =
context.modifiedQuery.slice(0, existingPair.position.valueStart) +
formattedValue +
context.modifiedQuery.slice(existingPair.position.valueEnd + 1);
// Update the query pairs map
context.queryPairsMap = getQueryPairsMap(context.modifiedQuery);
return { type: 'update' };
}
// Handle operator transformations
return handleOperatorTransformations(filter, context, formattedValue);
}
function processDefaultOperator(
filter: TagFilterItem,
context: QueryProcessingContext,
): ProcessingResult {
const { key } = filter;
const { op } = filter;
const { value } = filter;
// Skip if key is not defined
if (!key || !key.key) {
return { type: 'add', shouldAddToNonExisting: true };
}
const pairKey = `${key.key}-${op}`.trim().toLowerCase();
if (context.queryPairsMap.has(pairKey)) {
const existingPair = context.queryPairsMap.get(pairKey);
context.visitedPairs.add(pairKey);
if (
existingPair &&
existingPair.position &&
existingPair.position.valueStart &&
existingPair.position.valueEnd
) {
const formattedValue = formatValueForExpression(value, op);
context.modifiedQuery =
context.modifiedQuery.slice(0, existingPair.position.valueStart) +
formattedValue +
context.modifiedQuery.slice(existingPair.position.valueEnd + 1);
context.queryPairsMap = getQueryPairsMap(context.modifiedQuery);
}
return { type: 'update' };
}
return { type: 'add', shouldAddToNonExisting: true };
}
// Factory function to get appropriate handler
function getOperatorHandler(
operator: string,
): (
filter: TagFilterItem,
context: QueryProcessingContext,
) => ProcessingResult {
if (isInOperator(operator)) {
return processInOperator;
}
return processDefaultOperator;
}
// Helper function to create new filter items from unvisited query pairs
const createNewFilterItems = (
context: QueryProcessingContext,
): TagFilterItem[] => {
const newFilterItems: TagFilterItem[] = [];
context.queryPairsMap.forEach((pair, key) => {
if (!context.visitedPairs.has(key)) {
const operator = pair.hasNegation
? getOperatorValue(`NOT_${pair.operator}`.toUpperCase())
: getOperatorValue(pair.operator.toUpperCase());
const formatValuesForFilter = (
value: string | string[],
): string | string[] => {
if (Array.isArray(value)) {
return value.map((v) => (typeof v === 'string' ? unquote(v) : String(v)));
}
if (typeof value === 'string') {
return unquote(value);
}
return String(value);
};
newFilterItems.push({
id: uuid(),
op: operator,
key: {
id: pair.key,
key: pair.key,
type: '',
},
value: pair.isMultiValue
? formatValuesForFilter(pair.valueList as string[]) ?? ''
: formatValuesForFilter(pair.value as string) ?? '',
});
}
});
return newFilterItems;
};
// Main refactored function
export const convertFiltersToExpressionWithExistingQuery = (
filters: TagFilter,
existingQuery: string | undefined,
): { filters: TagFilter; filter: { expression: string } } => {
// Early return for no existing query
if (!existingQuery) {
const normalizedFilters = normalizeDeprecatedOperators(filters);
const expression = convertFiltersToExpression(normalizedFilters);
return {
filters: normalizedFilters,
filter: expression,
};
}
// Validate inputs
if (!validateQuery(existingQuery)) {
return { filters, filter: { expression: existingQuery || '' } };
}
// Normalize deprecated operators
const normalizedFilters = normalizeDeprecatedOperators(filters);
// Initialize processing context
const context: QueryProcessingContext = {
originalQuery: existingQuery,
queryPairsMap: getQueryPairsMap(existingQuery.trim()),
visitedPairs: new Set(),
modifications: [],
newFilters: [],
nonExistingFilters: [],
modifiedQuery: existingQuery,
};
// Process each filter (if any exist)
if (normalizedFilters.items?.length > 0) {
normalizedFilters.items.filter(validateFilter).forEach((filter) => {
const handler = getOperatorHandler(filter.op);
const result = handler(filter, context);
// Apply result based on type
switch (result.type) {
case 'add':
if (result.shouldAddToNonExisting) {
context.nonExistingFilters.push(filter);
}
break;
case 'update':
case 'transform':
case 'skip':
// Already handled in the processor
break;
default:
// Handle any other cases
break;
}
});
}
// Create new filter items from unvisited query pairs
const newFilterItems = createNewFilterItems(context);
// Merge new filter items with existing ones
if (newFilterItems.length > 0) {
if (normalizedFilters?.items?.length > 0) {
// Add new filter items to existing ones
normalizedFilters.items = [...normalizedFilters.items, ...newFilterItems];
} else {
// Use new filter items as the main filters
normalizedFilters.items = newFilterItems;
}
}
// Build final expression
let finalExpression = context.modifiedQuery;
if (context.nonExistingFilters.length > 0) {
// Convert non-existing filters to expression and append
const nonExistingFilterExpression = convertFiltersToExpression({
items: context.nonExistingFilters,
op: filters.op || 'AND',
});
if (nonExistingFilterExpression.expression) {
finalExpression = `${context.modifiedQuery.trim()} ${
nonExistingFilterExpression.expression
}`;
}
}
return {
filters: normalizedFilters,
filter: { expression: finalExpression || '' },
};
};

View File

@@ -2,11 +2,9 @@
import { createAggregation } from 'api/v5/queryRange/prepareQueryRangePayloadV5';
import {
DEPRECATED_OPERATORS_MAP,
OPERATORS,
QUERY_BUILDER_FUNCTIONS,
} from 'constants/antlrQueryConstants';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { cloneDeep, isEqual, sortBy } from 'lodash-es';
import { IQueryPair } from 'types/antlrQueryTypes';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
@@ -174,298 +172,6 @@ export const convertExpressionToFilters = (
return filters;
};
const getQueryPairsMap = (query: string): Map<string, IQueryPair> => {
const queryPairs = extractQueryPairs(query);
const queryPairsMap: Map<string, IQueryPair> = new Map();
queryPairs.forEach((pair) => {
const key = pair.hasNegation
? `${pair.key}-not ${pair.operator}`.trim().toLowerCase()
: `${pair.key}-${pair.operator}`.trim().toLowerCase();
queryPairsMap.set(key, pair);
});
return queryPairsMap;
};
export const convertFiltersToExpressionWithExistingQuery = (
filters: TagFilter,
existingQuery: string | undefined,
): { filters: TagFilter; filter: { expression: string } } => {
// Check for deprecated operators and replace them with new operators
const updatedFilters = cloneDeep(filters);
// Replace deprecated operators in filter items
if (updatedFilters?.items) {
updatedFilters.items = updatedFilters.items.map((item) => {
const opLower = item.op?.toLowerCase();
if (Object.keys(DEPRECATED_OPERATORS_MAP).includes(opLower)) {
return {
...item,
op: DEPRECATED_OPERATORS_MAP[
opLower as keyof typeof DEPRECATED_OPERATORS_MAP
].toLowerCase(),
};
}
return item;
});
}
if (!existingQuery) {
// If no existing query, return filters with a newly generated expression
return {
filters: updatedFilters,
filter: convertFiltersToExpression(updatedFilters),
};
}
const nonExistingFilters: TagFilterItem[] = [];
let modifiedQuery = existingQuery; // We'll modify this query as we proceed
const visitedPairs: Set<string> = new Set(); // Set to track visited query pairs
// Map extracted query pairs to key-specific pair information for faster access
let queryPairsMap = getQueryPairsMap(existingQuery);
filters?.items?.forEach((filter) => {
const { key, op, value } = filter;
// Skip invalid filters with no key
if (!key) return;
let shouldAddToNonExisting = true; // Flag to decide if the filter should be added to non-existing filters
const sanitizedOperator = op.trim().toUpperCase();
// Check if the operator is IN or NOT IN
if (
[OPERATORS.IN, `${OPERATORS.NOT} ${OPERATORS.IN}`].includes(
sanitizedOperator,
)
) {
const existingPair = queryPairsMap.get(
`${key.key}-${op}`.trim().toLowerCase(),
);
const formattedValue = formatValueForExpression(value, op);
// If a matching query pair exists, modify the query
if (
existingPair &&
existingPair.position?.valueStart &&
existingPair.position?.valueEnd
) {
visitedPairs.add(`${key.key}-${op}`.trim().toLowerCase());
// Check if existing values match current filter values (for array-based operators)
if (existingPair.valueList && filter.value && Array.isArray(filter.value)) {
// Clean quotes from string values for comparison
const cleanValues = (values: any[]): any[] =>
values.map((val) => (typeof val === 'string' ? unquote(val) : val));
const cleanExistingValues = cleanValues(existingPair.valueList);
const cleanFilterValues = cleanValues(filter.value);
// Compare arrays (order-independent) - if identical, keep existing value
const isSameValues =
cleanExistingValues.length === cleanFilterValues.length &&
isEqual(sortBy(cleanExistingValues), sortBy(cleanFilterValues));
if (isSameValues) {
// Values are identical, preserve existing formatting
modifiedQuery =
modifiedQuery.slice(0, existingPair.position.valueStart) +
existingPair.value +
modifiedQuery.slice(existingPair.position.valueEnd + 1);
return;
}
}
modifiedQuery =
modifiedQuery.slice(0, existingPair.position.valueStart) +
formattedValue +
modifiedQuery.slice(existingPair.position.valueEnd + 1);
queryPairsMap = getQueryPairsMap(modifiedQuery);
return;
}
// Handle the different cases for IN operator
switch (sanitizedOperator) {
case OPERATORS.IN:
// If there's a NOT IN or equal operator, merge the filter
if (
queryPairsMap.has(
`${key.key}-${OPERATORS.NOT} ${op}`.trim().toLowerCase(),
)
) {
const notInPair = queryPairsMap.get(
`${key.key}-${OPERATORS.NOT} ${op}`.trim().toLowerCase(),
);
visitedPairs.add(
`${key.key}-${OPERATORS.NOT} ${op}`.trim().toLowerCase(),
);
if (notInPair?.position?.valueEnd) {
modifiedQuery = `${modifiedQuery.slice(
0,
notInPair.position.negationStart,
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
notInPair.position.valueEnd + 1,
)}`;
queryPairsMap = getQueryPairsMap(modifiedQuery);
}
shouldAddToNonExisting = false; // Don't add this to non-existing filters
} else if (
queryPairsMap.has(`${key.key}-${OPERATORS['=']}`.trim().toLowerCase())
) {
const equalsPair = queryPairsMap.get(
`${key.key}-${OPERATORS['=']}`.trim().toLowerCase(),
);
visitedPairs.add(`${key.key}-${OPERATORS['=']}`.trim().toLowerCase());
if (equalsPair?.position?.valueEnd) {
modifiedQuery = `${modifiedQuery.slice(
0,
equalsPair.position.operatorStart,
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
equalsPair.position.valueEnd + 1,
)}`;
queryPairsMap = getQueryPairsMap(modifiedQuery);
}
shouldAddToNonExisting = false; // Don't add this to non-existing filters
} else if (
queryPairsMap.has(`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase())
) {
const notEqualsPair = queryPairsMap.get(
`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase(),
);
visitedPairs.add(`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase());
if (notEqualsPair?.position?.valueEnd) {
modifiedQuery = `${modifiedQuery.slice(
0,
notEqualsPair.position.operatorStart,
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
notEqualsPair.position.valueEnd + 1,
)}`;
queryPairsMap = getQueryPairsMap(modifiedQuery);
}
shouldAddToNonExisting = false; // Don't add this to non-existing filters
}
break;
case `${OPERATORS.NOT} ${OPERATORS.IN}`:
if (
queryPairsMap.has(`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase())
) {
const notEqualsPair = queryPairsMap.get(
`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase(),
);
visitedPairs.add(`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase());
if (notEqualsPair?.position?.valueEnd) {
modifiedQuery = `${modifiedQuery.slice(
0,
notEqualsPair.position.operatorStart,
)}${OPERATORS.NOT} ${
OPERATORS.IN
} ${formattedValue} ${modifiedQuery.slice(
notEqualsPair.position.valueEnd + 1,
)}`;
queryPairsMap = getQueryPairsMap(modifiedQuery);
}
shouldAddToNonExisting = false; // Don't add this to non-existing filters
}
break; // No operation needed for NOT IN case
default:
break;
}
}
if (
queryPairsMap.has(`${filter.key?.key}-${filter.op}`.trim().toLowerCase())
) {
const existingPair = queryPairsMap.get(
`${filter.key?.key}-${filter.op}`.trim().toLowerCase(),
);
if (
existingPair &&
existingPair.position?.valueStart &&
existingPair.position?.valueEnd
) {
const formattedValue = formatValueForExpression(value, op);
// replace the value with the new value
modifiedQuery =
modifiedQuery.slice(0, existingPair.position.valueStart) +
formattedValue +
modifiedQuery.slice(existingPair.position.valueEnd + 1);
queryPairsMap = getQueryPairsMap(modifiedQuery);
}
visitedPairs.add(`${filter.key?.key}-${filter.op}`.trim().toLowerCase());
}
// Add filters that don't have an existing pair to non-existing filters
if (
shouldAddToNonExisting &&
!queryPairsMap.has(`${filter.key?.key}-${filter.op}`.trim().toLowerCase())
) {
nonExistingFilters.push(filter);
}
});
// Create new filters from non-visited query pairs
const newFilterItems: TagFilterItem[] = [];
queryPairsMap.forEach((pair, key) => {
if (!visitedPairs.has(key)) {
const operator = pair.hasNegation
? getOperatorValue(`NOT_${pair.operator}`.toUpperCase())
: getOperatorValue(pair.operator.toUpperCase());
newFilterItems.push({
id: uuid(),
op: operator,
key: {
id: pair.key,
key: pair.key,
type: '',
},
value: pair.isMultiValue
? formatValuesForFilter(pair.valueList as string[]) ?? ''
: formatValuesForFilter(pair.value as string) ?? '',
});
}
});
// Merge new filter items with existing ones
if (newFilterItems.length > 0 && updatedFilters?.items) {
updatedFilters.items = [...updatedFilters.items, ...newFilterItems];
}
// If no non-existing filters, return the modified query directly
if (nonExistingFilters.length === 0) {
return {
filters: updatedFilters,
filter: { expression: modifiedQuery },
};
}
// Convert non-existing filters to an expression and append to the modified query
const nonExistingFilterExpression = convertFiltersToExpression({
items: nonExistingFilters,
op: filters.op || 'AND',
});
if (nonExistingFilterExpression.expression) {
return {
filters: updatedFilters,
filter: {
expression: `${modifiedQuery.trim()} ${
nonExistingFilterExpression.expression
}`,
},
};
}
// Return the final result with the modified query
return {
filters: updatedFilters,
filter: { expression: modifiedQuery || '' },
};
};
/**
* Removes specified key-value pairs from a logical query expression string.

View File

@@ -2,7 +2,7 @@
import { Color } from '@signozhq/design-tokens';
import { Progress, Tag, Tooltip } from 'antd';
import { ColumnType } from 'antd/es/table';
import { convertFiltersToExpressionWithExistingQuery } from 'components/QueryBuilderV2/utils';
import { convertFiltersToExpressionWithExistingQuery } from 'components/QueryBuilderV2/queryProcessor';
import {
FiltersType,
IQuickFiltersConfig,

View File

@@ -1,8 +1,6 @@
/* eslint-disable sonarjs/cognitive-complexity */
import {
convertFiltersToExpressionWithExistingQuery,
removeKeysFromExpression,
} from 'components/QueryBuilderV2/utils';
import { convertFiltersToExpressionWithExistingQuery } from 'components/QueryBuilderV2/queryProcessor';
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';
import { cloneDeep, isArray, isEmpty } from 'lodash-es';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
import {

View File

@@ -1,5 +1,5 @@
import { PieArcDatum } from '@visx/shape/lib/shapes/Pie';
import { convertFiltersToExpressionWithExistingQuery } from 'components/QueryBuilderV2/utils';
import { convertFiltersToExpressionWithExistingQuery } from 'components/QueryBuilderV2/queryProcessor';
import {
initialQueryBuilderFormValuesMap,
OPERATORS,

View File

@@ -1,6 +1,6 @@
import { convertFiltersToExpressionWithExistingQuery } from 'components/QueryBuilderV2/queryProcessor';
import {
convertAggregationToExpression,
convertFiltersToExpressionWithExistingQuery,
convertHavingToExpression,
} from 'components/QueryBuilderV2/utils';
import { QueryParams } from 'constants/query';

View File

@@ -1,5 +1,5 @@
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
import { convertFiltersToExpressionWithExistingQuery } from 'components/QueryBuilderV2/utils';
import { convertFiltersToExpressionWithExistingQuery } from 'components/QueryBuilderV2/queryProcessor';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryBuilderKeys } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';