Compare commits
5 Commits
main
...
enh/conver
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db004aac4c | ||
|
|
0ecaa1779f | ||
|
|
4161a711de | ||
|
|
4de2783944 | ||
|
|
69f33d6fe3 |
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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', () => {
|
||||
|
||||
531
frontend/src/components/QueryBuilderV2/queryProcessor.ts
Normal file
531
frontend/src/components/QueryBuilderV2/queryProcessor.ts
Normal 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 || '' },
|
||||
};
|
||||
};
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { convertFiltersToExpressionWithExistingQuery } from 'components/QueryBuilderV2/queryProcessor';
|
||||
import {
|
||||
convertAggregationToExpression,
|
||||
convertFiltersToExpressionWithExistingQuery,
|
||||
convertHavingToExpression,
|
||||
} from 'components/QueryBuilderV2/utils';
|
||||
import { QueryParams } from 'constants/query';
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user