mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-15 12:58:33 +00:00
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
1602 lines
43 KiB
TypeScript
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]');
|
|
});
|
|
});
|
|
});
|