Files
o11y/frontend/src/components/QueryBuilderV2/__tests__/utils.test.ts
T
zeekay 46c6241c52 Spell every o11y caller /v1/o11y
The server moved off /api/vN; the integration suite, handler tests,
install.sh's health wait and the docs still called the old tree.

Claude-Session: https://claude.ai/code/session_01TUxvfuta1Z44KVNYuAz71F
2026-09-24 15:05:09 -07:00

1602 lines
43 KiB
TypeScript

import { negateOperator, OPERATORS } from 'constants/antlrQueryConstants';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { extractQueryPairs } from 'utils/queryContextUtils';
import {
convertAggregationToExpression,
convertFiltersToExpression,
convertFiltersToExpressionWithExistingQuery,
formatValueForExpression,
removeKeysFromExpression,
} from '../utils';
describe('convertFiltersToExpression', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should handle empty, null, and undefined inputs', () => {
// Test null and undefined
expect(convertFiltersToExpression(null as any)).toStrictEqual({
expression: '',
});
expect(convertFiltersToExpression(undefined as any)).toStrictEqual({
expression: '',
});
// Test empty filters
expect(convertFiltersToExpression({ items: [], op: 'AND' })).toStrictEqual({
expression: '',
});
expect(
convertFiltersToExpression({ items: undefined, op: 'AND' } as any),
).toStrictEqual({ expression: '' });
});
it('should convert basic comparison operators with proper value formatting', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'service', type: 'string' },
op: '=',
value: 'api-gateway',
},
{
id: '2',
key: { key: 'status', type: 'string' },
op: '!=',
value: 'error',
},
{
id: '3',
key: { key: 'duration', type: 'number' },
op: '>',
value: 100,
},
{
id: '4',
key: { key: 'count', type: 'number' },
op: '<=',
value: 50,
},
{
id: '5',
key: { key: 'is_active', type: 'boolean' },
op: '=',
value: true,
},
{
id: '6',
key: { key: 'enabled', type: 'boolean' },
op: '=',
value: false,
},
{
id: '7',
key: { key: 'count', type: 'number' },
op: '=',
value: 0,
},
{
id: '7',
key: { key: 'regex', type: 'string' },
op: 'regex',
value: '.*',
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toStrictEqual({
expression:
"service = 'api-gateway' AND status != 'error' AND duration > 100 AND count <= 50 AND is_active = true AND enabled = false AND count = 0 AND regex REGEXP '.*'",
});
});
it('should handle string value formatting and escaping', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'message', type: 'string' },
op: '=',
value: "user's data",
},
{
id: '2',
key: { key: 'description', type: 'string' },
op: '=',
value: '',
},
{
id: '3',
key: { key: 'path', type: 'string' },
op: '=',
value: '/v1/users',
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toStrictEqual({
expression:
"message = 'user\\'s data' AND description = '' AND path = '/v1/users'",
});
});
it('should handle IN operator with various value types and array formatting', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'service', type: 'string' },
op: 'IN',
value: ['api-gateway', 'user-service', 'auth-service'],
},
{
id: '2',
key: { key: 'status', type: 'string' },
op: 'IN',
value: 'success', // Single value should be converted to array
},
{
id: '3',
key: { key: 'tags', type: 'string' },
op: 'IN',
value: [], // Empty array
},
{
id: '4',
key: { key: 'name', type: 'string' },
op: 'IN',
value: ["John's", "Mary's", 'Bob'], // Values with quotes
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toStrictEqual({
expression:
"service in ['api-gateway', 'user-service', 'auth-service'] AND status in ['success'] AND tags in [] AND name in ['John\\'s', 'Mary\\'s', 'Bob']",
});
});
it('should convert deprecated operators to their modern equivalents', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'service', type: 'string' },
op: 'nin',
value: ['api-gateway', 'user-service'],
},
{
id: '2',
key: { key: 'message', type: 'string' },
op: 'nlike',
value: 'error',
},
{
id: '3',
key: { key: 'path', type: 'string' },
op: 'nregex',
value: '/api/.*',
},
{
id: '4',
key: { key: 'service', type: 'string' },
op: 'NIN', // Test case insensitivity
value: ['api-gateway'],
},
{
id: '5',
key: { key: 'user_id', type: 'string' },
op: 'nexists',
value: '',
},
{
id: '6',
key: { key: 'description', type: 'string' },
op: 'ncontains',
value: 'error',
},
{
id: '7',
key: { key: 'tags', type: 'string' },
op: 'nhas',
value: 'production',
},
{
id: '8',
key: { key: 'labels', type: 'string' },
op: 'nhasany',
value: ['env:prod', 'service:api'],
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toStrictEqual({
expression:
"service NOT IN ['api-gateway', 'user-service'] AND message NOT LIKE 'error' AND path NOT REGEXP '/api/.*' AND service NOT IN ['api-gateway'] AND user_id NOT EXISTS AND description NOT CONTAINS 'error' AND NOT has(tags, 'production') AND NOT hasAny(labels, ['env:prod', 'service:api'])",
});
});
it('should handle non-value operators and function operators', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'user_id', type: 'string' },
op: 'EXISTS',
value: '', // Value should be ignored for EXISTS
},
{
id: '2',
key: { key: 'user_id', type: 'string' },
op: 'EXISTS',
value: 'some-value', // Value should be ignored for EXISTS
},
{
id: '3',
key: { key: 'tags', type: 'string' },
op: 'has',
value: 'production',
},
{
id: '4',
key: { key: 'tags', type: 'string' },
op: 'hasAny',
value: ['production', 'staging'],
},
{
id: '5',
key: { key: 'tags', type: 'string' },
op: 'hasAll',
value: ['production', 'monitoring'],
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toStrictEqual({
expression:
"user_id exists AND user_id exists AND has(tags, 'production') AND hasAny(tags, ['production', 'staging']) AND hasAll(tags, ['production', 'monitoring'])",
});
});
it('should filter out invalid filters and handle edge cases', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'service', type: 'string' },
op: '=',
value: 'api-gateway',
},
{
id: '2',
key: undefined, // Invalid filter - should be skipped
op: '=',
value: 'error',
},
{
id: '3',
key: { key: '', type: 'string' }, // Invalid filter with empty key - should be skipped
op: '=',
value: 'test',
},
{
id: '4',
key: { key: 'status', type: 'string' },
op: ' = ', // Test whitespace handling
value: 'success',
},
{
id: '5',
key: { key: 'service', type: 'string' },
op: 'In', // Test mixed case handling
value: ['api-gateway'],
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toStrictEqual({
expression:
"service = 'api-gateway' AND status = 'success' AND service in ['api-gateway']",
});
});
it('should handle complex mixed operator scenarios with proper joining', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'service', type: 'string' },
op: 'IN',
value: ['api-gateway', 'user-service'],
},
{
id: '2',
key: { key: 'user_id', type: 'string' },
op: 'EXISTS',
value: '',
},
{
id: '3',
key: { key: 'tags', type: 'string' },
op: 'has',
value: 'production',
},
{
id: '4',
key: { key: 'duration', type: 'number' },
op: '>',
value: 100,
},
{
id: '5',
key: { key: 'status', type: 'string' },
op: 'nin',
value: ['error', 'timeout'],
},
{
id: '6',
key: { key: 'method', type: 'string' },
op: '=',
value: 'POST',
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toStrictEqual({
expression:
"service in ['api-gateway', 'user-service'] AND user_id exists AND has(tags, 'production') AND duration > 100 AND status NOT IN ['error', 'timeout'] AND method = 'POST'",
});
});
it('should handle all numeric comparison operators and edge cases', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'count', type: 'number' },
op: '=',
value: 0,
},
{
id: '2',
key: { key: 'score', type: 'number' },
op: '>',
value: 100,
},
{
id: '3',
key: { key: 'limit', type: 'number' },
op: '>=',
value: 50,
},
{
id: '4',
key: { key: 'threshold', type: 'number' },
op: '<',
value: 1000,
},
{
id: '5',
key: { key: 'max_value', type: 'number' },
op: '<=',
value: 999,
},
{
id: '6',
key: { key: 'values', type: 'string' },
op: 'IN',
value: ['1', '2', '3', '4', '5'],
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toStrictEqual({
expression:
"count = 0 AND score > 100 AND limit >= 50 AND threshold < 1000 AND max_value <= 999 AND values in ['1', '2', '3', '4', '5']",
});
});
it('should handle boolean values and string comparisons with special characters', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'is_active', type: 'boolean' },
op: '=',
value: true,
},
{
id: '2',
key: { key: 'is_deleted', type: 'boolean' },
op: '=',
value: false,
},
{
id: '3',
key: { key: 'email', type: 'string' },
op: '=',
value: '[email protected]',
},
{
id: '4',
key: { key: 'description', type: 'string' },
op: '=',
value: 'Contains "quotes" and \'apostrophes\'',
},
{
id: '5',
key: { key: 'path', type: 'string' },
op: '=',
value: '/v1/users/123?filter=true',
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toStrictEqual({
expression:
"is_active = true AND is_deleted = false AND email = '[email protected]' AND description = 'Contains \"quotes\" and \\'apostrophes\\'' AND path = '/v1/users/123?filter=true'",
});
});
it('should handle all function operators and complex array scenarios', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'tags', type: 'string' },
op: 'has',
value: 'production',
},
{
id: '2',
key: { key: 'labels', type: 'string' },
op: 'hasAny',
value: ['env:prod', 'service:api'],
},
{
id: '3',
key: { key: 'metadata', type: 'string' },
op: 'hasAll',
value: ['version:1.0', 'team:backend'],
},
{
id: '4',
key: { key: 'services', type: 'string' },
op: 'IN',
value: ['api-gateway', 'user-service', 'auth-service', 'payment-service'],
},
{
id: '5',
key: { key: 'excluded_services', type: 'string' },
op: 'nin',
value: ['legacy-service', 'deprecated-service'],
},
{
id: '6',
key: { key: 'status_codes', type: 'string' },
op: 'IN',
value: ['200', '201', '400', '500'],
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toStrictEqual({
expression:
"has(tags, 'production') AND hasAny(labels, ['env:prod', 'service:api']) AND hasAll(metadata, ['version:1.0', 'team:backend']) AND services in ['api-gateway', 'user-service', 'auth-service', 'payment-service'] AND excluded_services NOT IN ['legacy-service', 'deprecated-service'] AND status_codes in ['200', '201', '400', '500']",
});
});
it('should handle specific deprecated operators: nhas, ncontains, nexists', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'user_id', type: 'string' },
op: 'nexists',
value: '',
},
{
id: '2',
key: { key: 'description', type: 'string' },
op: 'ncontains',
value: 'error',
},
{
id: '3',
key: { key: 'tags', type: 'string' },
op: 'nhas',
value: 'production',
},
{
id: '4',
key: { key: 'labels', type: 'string' },
op: 'nhasany',
value: ['env:prod', 'service:api'],
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toStrictEqual({
expression:
"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).toStrictEqual(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).toStrictEqual(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)).toStrictEqual(
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', () => {
const mockAttribute: BaseAutocompleteData = {
id: 'test-id',
key: 'test_metric',
type: 'string',
dataType: DataTypes.String,
};
it('should return undefined when no aggregateOperator is provided', () => {
const result = convertAggregationToExpression({
aggregateOperator: '',
aggregateAttribute: mockAttribute,
dataSource: DataSource.METRICS,
});
expect(result).toBeUndefined();
});
it('should convert metrics aggregation with required temporality field', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'sum',
aggregateAttribute: mockAttribute,
dataSource: DataSource.METRICS,
timeAggregation: 'avg',
spaceAggregation: 'max',
alias: 'test_alias',
reduceTo: ReduceOperators.SUM,
temporality: 'delta',
});
expect(result).toStrictEqual([
{
metricName: 'test_metric',
timeAggregation: 'avg',
spaceAggregation: 'max',
reduceTo: ReduceOperators.SUM,
temporality: 'delta',
},
]);
});
it('should handle noop operators by converting to count', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'noop',
aggregateAttribute: mockAttribute,
dataSource: DataSource.METRICS,
timeAggregation: 'noop',
spaceAggregation: 'noop',
});
expect(result).toStrictEqual([
{
metricName: 'test_metric',
reduceTo: undefined,
temporality: undefined,
timeAggregation: 'count',
spaceAggregation: 'count',
},
]);
});
it('should handle missing attribute key gracefully', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'sum',
aggregateAttribute: { ...mockAttribute, key: '' },
dataSource: DataSource.METRICS,
});
expect(result).toStrictEqual([
{
metricName: '',
reduceTo: undefined,
temporality: undefined,
timeAggregation: 'sum',
spaceAggregation: 'sum',
},
]);
});
it('should convert traces aggregation to expression format', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'count',
aggregateAttribute: mockAttribute,
dataSource: DataSource.TRACES,
alias: 'trace_alias',
});
expect(result).toStrictEqual([
{
expression: 'count(test_metric)',
alias: 'trace_alias',
},
]);
});
it('should convert logs aggregation to expression format', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'avg',
aggregateAttribute: mockAttribute,
dataSource: DataSource.LOGS,
alias: 'log_alias',
});
expect(result).toStrictEqual([
{
expression: 'avg(test_metric)',
alias: 'log_alias',
},
]);
});
it('should handle aggregation without attribute key for traces/logs', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'count',
aggregateAttribute: { ...mockAttribute, key: '' },
dataSource: DataSource.TRACES,
});
expect(result).toStrictEqual([
{
expression: 'count()',
},
]);
});
it('should handle missing alias for traces/logs', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'sum',
aggregateAttribute: mockAttribute,
dataSource: DataSource.LOGS,
});
expect(result).toStrictEqual([
{
expression: 'sum(test_metric)',
},
]);
});
it('should use aggregateOperator as fallback for time and space aggregation', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'max',
aggregateAttribute: mockAttribute,
dataSource: DataSource.METRICS,
});
expect(result).toStrictEqual([
{
metricName: 'test_metric',
reduceTo: undefined,
temporality: undefined,
timeAggregation: 'max',
spaceAggregation: 'max',
},
]);
});
it('should handle undefined aggregateAttribute parameter with metrics', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'sum',
aggregateAttribute: mockAttribute,
dataSource: DataSource.METRICS,
});
expect(result).toStrictEqual([
{
metricName: 'test_metric',
timeAggregation: 'sum',
spaceAggregation: 'sum',
reduceTo: undefined,
temporality: undefined,
},
]);
});
it('should handle undefined aggregateAttribute parameter with traces', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'noop',
aggregateAttribute: undefined as unknown as BaseAutocompleteData,
dataSource: DataSource.TRACES,
});
expect(result).toStrictEqual([
{
expression: 'count()',
},
]);
});
it('should handle undefined aggregateAttribute parameter with logs', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'noop',
aggregateAttribute: undefined as unknown as BaseAutocompleteData,
dataSource: DataSource.LOGS,
});
expect(result).toStrictEqual([
{
expression: 'count()',
},
]);
});
});
describe('removeKeysFromExpression', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('Backward compatibility (removeOnlyVariableExpressions = false)', () => {
it('should remove simple key-value pair from expression', () => {
const expression = "service.name = 'api-gateway' AND status = 'success'";
const result = removeKeysFromExpression(expression, ['service.name']);
expect(result).toBe("status = 'success'");
});
it('should remove multiple keys from expression', () => {
const expression =
"service.name = 'api-gateway' AND status = 'success' AND region = 'us-east-1'";
const result = removeKeysFromExpression(expression, [
'service.name',
'status',
]);
expect(result).toBe("region = 'us-east-1'");
});
it('should handle empty expression', () => {
const result = removeKeysFromExpression('', ['service.name']);
expect(result).toBe('');
});
it('should handle empty keys array', () => {
const expression = "service.name = 'api-gateway'";
const result = removeKeysFromExpression(expression, []);
expect(result).toBe(expression);
});
it('should handle key not found in expression', () => {
const expression = "service.name = 'api-gateway'";
const result = removeKeysFromExpression(expression, ['nonexistent.key']);
expect(result).toBe(expression);
});
// todo: Sagar check this - this is expected or not
// it('should remove last occurrence when multiple occurrences exist', () => {
// // This tests the original behavior - should remove the last occurrence
// const expression =
// "deployment.environment = $deployment.environment deployment.environment = 'default'";
// const result = removeKeysFromExpression(
// expression,
// ['deployment.environment'],
// false,
// );
// // Should remove the literal value (last occurrence), leaving the variable
// expect(result).toBe('deployment.environment = $deployment.environment');
// });
});
describe('Variable expression targeting (removeOnlyVariableExpressions = true)', () => {
it('should remove only variable expressions (values starting with $)', () => {
const expression =
"deployment.environment = $deployment.environment deployment.environment = 'default'";
const result = removeKeysFromExpression(
expression,
['deployment.environment'],
true,
);
// Should remove the variable expression, leaving the literal value
expect(result).toBe("deployment.environment = 'default'");
});
it('should not remove literal values when targeting variable expressions', () => {
const expression = "service.name = 'api-gateway' AND status = 'success'";
const result = removeKeysFromExpression(expression, ['service.name'], true);
// Should not remove anything since no variable expressions exist
expect(result).toBe("service.name = 'api-gateway' AND status = 'success'");
});
it('should remove multiple variable expressions', () => {
const expression =
"deployment.environment = $deployment.environment service.name = $service.name status = 'success'";
const result = removeKeysFromExpression(
expression,
['deployment.environment', 'service.name'],
true,
);
expect(result).toBe("status = 'success'");
});
it('should handle mixed variable and literal expressions correctly', () => {
const expression =
"deployment.environment = $deployment.environment service.name = 'api-gateway' region = $region";
const result = removeKeysFromExpression(
expression,
['deployment.environment', 'region'],
true,
);
// Should only remove variable expressions, leaving literal value
expect(result).toBe("service.name = 'api-gateway'");
});
it('should handle complex expressions with operators', () => {
const expression =
"deployment.environment IN [$env1, $env2] AND service.name = 'api-gateway'";
const result = removeKeysFromExpression(
expression,
['deployment.environment'],
true,
);
expect(result).toBe("service.name = 'api-gateway'");
});
});
describe('Edge cases and robustness', () => {
it('should handle case insensitive key matching', () => {
const expression = 'Service.Name = $Service.Name';
const result = removeKeysFromExpression(expression, ['service.name'], true);
expect(result).toBe('');
});
it('should clean up trailing AND/OR operators', () => {
const expression =
"deployment.environment = $deployment.environment AND service.name = 'api-gateway'";
const result = removeKeysFromExpression(
expression,
['deployment.environment'],
true,
);
expect(result).toBe("service.name = 'api-gateway'");
});
it('should clean up leading AND/OR operators', () => {
const expression =
"service.name = 'api-gateway' AND deployment.environment = $deployment.environment";
const result = removeKeysFromExpression(
expression,
['deployment.environment'],
true,
);
expect(result).toBe("service.name = 'api-gateway'");
});
it('should handle expressions with only variable assignments', () => {
const expression = 'deployment.environment = $deployment.environment';
const result = removeKeysFromExpression(
expression,
['deployment.environment'],
true,
);
expect(result).toBe('');
});
it('should handle whitespace around operators', () => {
const expression =
"deployment.environment = $deployment.environment AND service.name = 'api-gateway'";
const result = removeKeysFromExpression(
expression,
['deployment.environment'],
true,
);
expect(result.trim()).toBe("service.name = 'api-gateway'");
});
});
describe('Real-world scenarios', () => {
it('should remove at most one variable expression per key', () => {
const expression =
"deployment.environment = $env1 deployment.environment = $env2 deployment.environment = 'default'";
const result = removeKeysFromExpression(
expression,
['deployment.environment'],
true,
);
// Should remove one occurrence — having multiple $-value clauses for the
// same key is invalid. The first is removed; subsequent $ clauses and
// literal-value clauses are preserved.
expect(result).toBe(
"deployment.environment = $env2 AND deployment.environment = 'default'",
);
});
it('should handle OR operators in expressions', () => {
const expression =
"deployment.environment = $deployment.environment OR service.name = 'api-gateway'";
const result = removeKeysFromExpression(
expression,
['deployment.environment'],
true,
);
expect(result).toBe("service.name = 'api-gateway'");
});
it('should maintain expression validity after removal', () => {
const expression =
"deployment.environment = $deployment.environment AND service.name = 'api-gateway' AND status = 'success'";
const result = removeKeysFromExpression(
expression,
['deployment.environment'],
true,
);
// Should maintain valid AND structure
expect(result).toBe("service.name = 'api-gateway' AND status = 'success'");
// Verify the result can be parsed by extractQueryPairs
const pairs = extractQueryPairs(result);
expect(pairs).toHaveLength(2);
});
});
describe('ANTLR-based removal — operator precedence (AND binds tighter than OR)', () => {
it('preserves OR when removing from a mixed AND/OR expression', () => {
// a AND b OR c — grammar parses as (a AND b) OR c
// removing b collapses the AND group to just a, OR is preserved
expect(
removeKeysFromExpression("a = '1' AND b = '2' OR c = '3'", ['b']),
).toBe("a = '1' OR c = '3'");
});
it('preserves correct conjunctions in a four-term mixed expression', () => {
// a AND b OR c AND d — removing b collapses first AND group to a
expect(
removeKeysFromExpression("a = '1' AND b = '2' OR c = '3' AND d = '4'", [
'b',
]),
).toBe("a = '1' OR c = '3' AND d = '4'");
});
it('preserves correct conjunctions when removing from a trailing AND group', () => {
// a OR b AND c OR d — removing c collapses second AND group to b
expect(
removeKeysFromExpression("a = '1' OR b = '2' AND c = '3' OR d = '4'", [
'c',
]),
).toBe("a = '1' OR b = '2' OR d = '4'");
});
});
describe('ANTLR-based removal — parenthesised expressions', () => {
it('removes last clause without leaving a dangling AND', () => {
const expression =
'(deployment.environment = $deployment.environment AND service.name = $service.name AND top_level_operation IN [$top_level_operation])';
expect(
removeKeysFromExpression(expression, ['top_level_operation'], true),
).toBe(
'(deployment.environment = $deployment.environment AND service.name = $service.name)',
);
});
it('removes first clause without leaving a dangling AND', () => {
expect(
removeKeysFromExpression(
'(deployment.environment = $deployment.environment AND service.name = $service.name)',
['deployment.environment'],
true,
),
).toBe('service.name = $service.name');
});
it('removes middle clause without disturbing surrounding AND', () => {
expect(
removeKeysFromExpression(
'(deployment.environment = $deployment.environment AND service.name = $service.name AND region = $region)',
['service.name'],
true,
),
).toBe(
'(deployment.environment = $deployment.environment AND region = $region)',
);
});
it('drops the empty paren group when its only child is removed', () => {
// (a) OR (b) — removing a must not leave () OR (b = '2')
// The remaining single-clause group has its redundant parens stripped too
expect(removeKeysFromExpression("(a = '1') OR (b = '2')", ['a'])).toBe(
"b = '2'",
);
});
it('handles OR inside parentheses without leaving dangling OR', () => {
expect(
removeKeysFromExpression(
'(service.name = $service.name OR operation = $operation)',
['operation'],
true,
),
).toBe('service.name = $service.name');
});
});
describe('ANTLR-based removal — BETWEEN, EXISTS, and other operators', () => {
it('removes a BETWEEN clause without treating its AND as a conjunction', () => {
// BETWEEN x AND y — the AND is part of the operator, not a conjunction
expect(
removeKeysFromExpression("a BETWEEN 1 AND 10 AND b = '2'", ['a']),
).toBe("b = '2'");
});
it('removes a NOT BETWEEN clause without treating its AND as a conjunction', () => {
expect(
removeKeysFromExpression("a NOT BETWEEN 1 AND 10 AND b = '2'", ['a']),
).toBe("b = '2'");
});
it('removes an EXISTS clause (no value token)', () => {
expect(removeKeysFromExpression("a = '1' AND b EXISTS", ['b'])).toBe(
"a = '1'",
);
});
it('removes a NOT EXISTS clause', () => {
expect(removeKeysFromExpression("a = '1' AND b NOT EXISTS", ['b'])).toBe(
"a = '1'",
);
});
it('removes an IN clause correctly', () => {
expect(
removeKeysFromExpression("service IN ['api', 'web'] AND status = 'ok'", [
'service',
]),
).toBe("status = 'ok'");
});
it('removes a NOT IN clause correctly', () => {
expect(
removeKeysFromExpression(
"service NOT IN ['api', 'web'] AND status = 'ok'",
['service'],
),
).toBe("status = 'ok'");
});
it('removes a CONTAINS clause correctly', () => {
expect(
removeKeysFromExpression("message CONTAINS 'error' AND service = 'api'", [
'message',
]),
).toBe("service = 'api'");
});
it('removes a LIKE clause correctly', () => {
expect(
removeKeysFromExpression("message LIKE '%error%' AND service = 'api'", [
'message',
]),
).toBe("service = 'api'");
});
it('removes a NOT LIKE clause correctly', () => {
expect(
removeKeysFromExpression("message NOT LIKE '%error%' AND service = 'api'", [
'message',
]),
).toBe("service = 'api'");
});
});
describe('ANTLR-based removal — AND/OR precedence combinations', () => {
it('handles a AND b AND c OR d: removing b leaves a AND c OR d', () => {
// AND binds tighter than OR, so this parses as (a AND b AND c) OR d
expect(
removeKeysFromExpression("a = '1' AND b = '2' AND c = '3' OR d = '4'", [
'b',
]),
).toBe("a = '1' AND c = '3' OR d = '4'");
});
});
describe('ANTLR-based removal — deeply nested expressions', () => {
const nestedExpr =
"((deployment.environment = $env1 OR deployment.environment = 'default') AND service.name = $svc1)";
it('removes service.name variable — outer and inner single-child parens both drop', () => {
// After removal: inner OR group keeps parens (2 items), outer group drops
// parens (1 item remains)
expect(removeKeysFromExpression(nestedExpr, ['service.name'], true)).toBe(
"(deployment.environment = $env1 OR deployment.environment = 'default')",
);
});
it('removes deployment.environment variable — inner OR collapses, outer parens kept', () => {
// Only the $env1 variable clause is removed; 'default' literal stays.
// Inner paren drops (single item left), outer paren stays (2 AND items remain).
expect(
removeKeysFromExpression(nestedExpr, ['deployment.environment'], true),
).toBe("(deployment.environment = 'default' AND service.name = $svc1)");
});
});
});
describe('formatValueForExpression', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('Variable values', () => {
it('should return variable values as-is', () => {
expect(formatValueForExpression('$variable')).toBe('$variable');
expect(formatValueForExpression('$env')).toBe('$env');
expect(formatValueForExpression(' $variable ')).toBe(' $variable ');
});
it('should return variable arrays as-is', () => {
expect(formatValueForExpression(['$var1', '$var2'])).toBe('$var1,$var2');
});
});
describe('Numeric string values', () => {
it('should return numeric strings with quotes', () => {
expect(formatValueForExpression('123')).toBe("'123'");
expect(formatValueForExpression('0')).toBe("'0'");
expect(formatValueForExpression('100000')).toBe("'100000'");
expect(formatValueForExpression('-42')).toBe("'-42'");
expect(formatValueForExpression('3.14')).toBe("'3.14'");
expect(formatValueForExpression(' 456 ')).toBe("' 456 '");
});
it('should handle numeric strings with IN operator', () => {
expect(formatValueForExpression('123', 'IN')).toBe("['123']");
expect(formatValueForExpression(['123', '456'], 'IN')).toBe(
"['123', '456']",
);
});
});
describe('Quoted string values', () => {
it('should return already quoted strings as-is', () => {
expect(formatValueForExpression("'quoted'")).toBe("'quoted'");
expect(formatValueForExpression('"double-quoted"')).toBe('"double-quoted"');
expect(formatValueForExpression('`backticked`')).toBe('`backticked`');
expect(formatValueForExpression("'100000'")).toBe("'100000'");
});
it('should preserve quoted strings in arrays', () => {
expect(formatValueForExpression(["'value1'", "'value2'"])).toBe(
"['value1', 'value2']",
);
expect(formatValueForExpression(["'100000'", "'200000'"], 'IN')).toBe(
"['100000', '200000']",
);
});
});
describe('Regular string values', () => {
it('should wrap regular strings in single quotes', () => {
expect(formatValueForExpression('hello')).toBe("'hello'");
expect(formatValueForExpression('api-gateway')).toBe("'api-gateway'");
expect(formatValueForExpression('test value')).toBe("'test value'");
});
it('should escape single quotes in strings', () => {
expect(formatValueForExpression("user's data")).toBe("'user\\'s data'");
expect(formatValueForExpression("John's")).toBe("'John\\'s'");
expect(formatValueForExpression("it's a test")).toBe("'it\\'s a test'");
});
it('should handle empty strings', () => {
expect(formatValueForExpression('')).toBe("''");
});
it('should handle strings with special characters', () => {
expect(formatValueForExpression('/v1/users')).toBe("'/v1/users'");
expect(formatValueForExpression('[email protected]')).toBe(
"'[email protected]'",
);
expect(formatValueForExpression('Contains "quotes"')).toBe(
'\'Contains "quotes"\'',
);
});
});
describe('Number values', () => {
it('should convert numbers to strings without quotes', () => {
expect(formatValueForExpression(123)).toBe('123');
expect(formatValueForExpression(0)).toBe('0');
expect(formatValueForExpression(-42)).toBe('-42');
expect(formatValueForExpression(100000)).toBe('100000');
expect(formatValueForExpression(3.14)).toBe('3.14');
});
it('should handle numbers with IN operator', () => {
expect(formatValueForExpression(123, 'IN')).toBe('[123]');
expect(formatValueForExpression([100, 200] as any, 'IN')).toBe('[100, 200]');
});
});
describe('Boolean values', () => {
it('should convert booleans to strings without quotes', () => {
expect(formatValueForExpression(true)).toBe('true');
expect(formatValueForExpression(false)).toBe('false');
});
it('should handle booleans with IN operator', () => {
expect(formatValueForExpression(true, 'IN')).toBe('[true]');
expect(formatValueForExpression([true, false] as any, 'IN')).toBe(
'[true, false]',
);
});
});
describe('Array values', () => {
it('should format array of strings', () => {
expect(formatValueForExpression(['a', 'b', 'c'])).toBe("['a', 'b', 'c']");
expect(formatValueForExpression(['service1', 'service2'])).toBe(
"['service1', 'service2']",
);
});
it('should format array of numeric strings', () => {
expect(formatValueForExpression(['123', '456', '789'])).toBe(
"['123', '456', '789']",
);
});
it('should format array of numbers', () => {
expect(formatValueForExpression([1, 2, 3] as any)).toBe('[1, 2, 3]');
expect(formatValueForExpression([100, 200, 300] as any)).toBe(
'[100, 200, 300]',
);
});
it('should format mixed array types', () => {
expect(formatValueForExpression(['hello', 123, true] as any)).toBe(
"['hello', 123, true]",
);
});
it('should format array with quoted values', () => {
expect(formatValueForExpression(["'quoted'", 'regular'])).toBe(
"['quoted', 'regular']",
);
});
it('should format array with empty strings', () => {
expect(formatValueForExpression(['', 'value'])).toBe("['', 'value']");
});
});
describe('IN and NOT IN operators', () => {
it('should format single value as array for IN operator', () => {
expect(formatValueForExpression('value', 'IN')).toBe("['value']");
expect(formatValueForExpression(123, 'IN')).toBe('[123]');
expect(formatValueForExpression('123', 'IN')).toBe("['123']");
});
it('should format array for IN operator', () => {
expect(formatValueForExpression(['a', 'b'], 'IN')).toBe("['a', 'b']");
expect(formatValueForExpression(['123', '456'], 'IN')).toBe(
"['123', '456']",
);
});
it('should format single value as array for NOT IN operator', () => {
expect(formatValueForExpression('value', 'NOT IN')).toBe("['value']");
expect(formatValueForExpression('value', 'not in')).toBe("['value']");
});
it('should format array for NOT IN operator', () => {
expect(formatValueForExpression(['a', 'b'], 'NOT IN')).toBe("['a', 'b']");
});
});
describe('Edge cases', () => {
it('should handle strings that look like numbers but have quotes', () => {
expect(formatValueForExpression("'123'")).toBe("'123'");
expect(formatValueForExpression('"456"')).toBe('"456"');
expect(formatValueForExpression('`789`')).toBe('`789`');
});
it('should handle strings with leading/trailing whitespace', () => {
expect(formatValueForExpression(' hello ')).toBe("' hello '");
expect(formatValueForExpression(' 123 ')).toBe("' 123 '");
});
it('should handle very large numbers', () => {
expect(formatValueForExpression('999999999')).toBe("'999999999'");
expect(formatValueForExpression(999999999)).toBe('999999999');
});
it('should handle decimal numbers', () => {
expect(formatValueForExpression('123.456')).toBe("'123.456'");
expect(formatValueForExpression(123.456)).toBe('123.456');
});
it('should handle negative numbers', () => {
expect(formatValueForExpression('-100')).toBe("'-100'");
expect(formatValueForExpression(-100)).toBe('-100');
});
it('should handle strings that are not valid numbers', () => {
expect(formatValueForExpression('123abc')).toBe("'123abc'");
expect(formatValueForExpression('abc123')).toBe("'abc123'");
expect(formatValueForExpression('12.34.56')).toBe("'12.34.56'");
});
it('should handle empty array', () => {
expect(formatValueForExpression([])).toBe('[]');
expect(formatValueForExpression([], 'IN')).toBe('[]');
});
it('should handle array with single element', () => {
expect(formatValueForExpression(['single'])).toBe("['single']");
expect(formatValueForExpression([123] as any)).toBe('[123]');
});
});
});