@gblikas/querykit 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,6 +15,35 @@ export declare class QueryParser implements IQueryParser {
15
15
  * Parse a query string into a QueryKit AST
16
16
  */
17
17
  parse(query: string): QueryExpression;
18
+ /**
19
+ * Pre-process a query string to convert non-standard syntax to Liqe-compatible syntax.
20
+ * Supports:
21
+ * - `field:[val1, val2, val3]` → `(field:val1 OR field:val2 OR field:val3)`
22
+ *
23
+ * This keeps the syntax consistent with the `key:value` pattern used throughout QueryKit:
24
+ * - `priority:>2` (comparison)
25
+ * - `status:active` (equality)
26
+ * - `status:[todo, doing, done]` (IN / multiple values)
27
+ */
28
+ private preprocessQuery;
29
+ /**
30
+ * Convert a field and comma-separated values to an OR expression string
31
+ */
32
+ private convertToOrExpression;
33
+ /**
34
+ * Parse a comma-separated string into values, respecting quoted strings.
35
+ * Commas inside quoted strings are preserved as part of the value.
36
+ *
37
+ * Examples:
38
+ * - `a, b, c` → ['a', 'b', 'c']
39
+ * - `"John, Jr.", Jane` → ['"John, Jr."', 'Jane']
40
+ * - `'hello, world', test` → ["'hello, world'", 'test']
41
+ */
42
+ private parseCommaSeparatedValues;
43
+ /**
44
+ * Format a field:value pair, quoting the value if necessary
45
+ */
46
+ private formatFieldValue;
18
47
  /**
19
48
  * Validate a query string
20
49
  */
@@ -35,6 +64,11 @@ export declare class QueryParser implements IQueryParser {
35
64
  * Create a comparison expression
36
65
  */
37
66
  private createComparisonExpression;
67
+ /**
68
+ * Convert a Liqe RangeExpression to a QueryKit logical AND expression
69
+ * E.g., `field:[2 TO 5]` becomes `(field >= 2 AND field <= 5)`
70
+ */
71
+ private convertRangeExpression;
38
72
  /**
39
73
  * Convert a Liqe operator to a QueryKit operator
40
74
  */
@@ -27,19 +27,142 @@ class QueryParser {
27
27
  */
28
28
  parse(query) {
29
29
  try {
30
- const liqeAst = (0, liqe_1.parse)(query);
30
+ // Pre-process the query to handle IN operator syntax
31
+ const preprocessedQuery = this.preprocessQuery(query);
32
+ const liqeAst = (0, liqe_1.parse)(preprocessedQuery);
31
33
  return this.convertLiqeAst(liqeAst);
32
34
  }
33
35
  catch (error) {
34
36
  throw new QueryParseError(`Failed to parse query: ${error instanceof Error ? error.message : String(error)}`);
35
37
  }
36
38
  }
39
+ /**
40
+ * Pre-process a query string to convert non-standard syntax to Liqe-compatible syntax.
41
+ * Supports:
42
+ * - `field:[val1, val2, val3]` → `(field:val1 OR field:val2 OR field:val3)`
43
+ *
44
+ * This keeps the syntax consistent with the `key:value` pattern used throughout QueryKit:
45
+ * - `priority:>2` (comparison)
46
+ * - `status:active` (equality)
47
+ * - `status:[todo, doing, done]` (IN / multiple values)
48
+ */
49
+ preprocessQuery(query) {
50
+ let result = query;
51
+ // Handle `field:[val1, val2, ...]` syntax (array-like, not range)
52
+ // Pattern: fieldName:[value1, value2, ...]
53
+ // We distinguish from range by checking for commas without "TO"
54
+ const bracketArrayPattern = /(\w+):\[([^\]]+)\]/g;
55
+ result = result.replace(bracketArrayPattern, (fullMatch, field, values) => {
56
+ // Check if this looks like a range expression (contains " TO ")
57
+ if (/\s+TO\s+/i.test(values)) {
58
+ // This is a range expression, keep as-is
59
+ return fullMatch;
60
+ }
61
+ // This is an array-like expression, convert to OR
62
+ return this.convertToOrExpression(field, values);
63
+ });
64
+ return result;
65
+ }
66
+ /**
67
+ * Convert a field and comma-separated values to an OR expression string
68
+ */
69
+ convertToOrExpression(field, valuesStr) {
70
+ // Parse values respecting quoted strings (commas inside quotes are preserved)
71
+ const values = this.parseCommaSeparatedValues(valuesStr);
72
+ if (values.length === 0) {
73
+ return `${field}:""`;
74
+ }
75
+ if (values.length === 1) {
76
+ return this.formatFieldValue(field, values[0]);
77
+ }
78
+ // Build OR expression: (field:val1 OR field:val2 OR ...)
79
+ const orClauses = values.map((v) => this.formatFieldValue(field, v));
80
+ return `(${orClauses.join(' OR ')})`;
81
+ }
82
+ /**
83
+ * Parse a comma-separated string into values, respecting quoted strings.
84
+ * Commas inside quoted strings are preserved as part of the value.
85
+ *
86
+ * Examples:
87
+ * - `a, b, c` → ['a', 'b', 'c']
88
+ * - `"John, Jr.", Jane` → ['"John, Jr."', 'Jane']
89
+ * - `'hello, world', test` → ["'hello, world'", 'test']
90
+ */
91
+ parseCommaSeparatedValues(input) {
92
+ const values = [];
93
+ let current = '';
94
+ let inDoubleQuotes = false;
95
+ let inSingleQuotes = false;
96
+ let i = 0;
97
+ while (i < input.length) {
98
+ const char = input[i];
99
+ const nextChar = input[i + 1];
100
+ // Handle escape sequences inside quotes
101
+ if ((inDoubleQuotes || inSingleQuotes) && char === '\\' && nextChar) {
102
+ // Include both the backslash and the escaped character
103
+ current += char + nextChar;
104
+ i += 2;
105
+ continue;
106
+ }
107
+ // Toggle double quote state
108
+ if (char === '"' && !inSingleQuotes) {
109
+ inDoubleQuotes = !inDoubleQuotes;
110
+ current += char;
111
+ i++;
112
+ continue;
113
+ }
114
+ // Toggle single quote state
115
+ if (char === "'" && !inDoubleQuotes) {
116
+ inSingleQuotes = !inSingleQuotes;
117
+ current += char;
118
+ i++;
119
+ continue;
120
+ }
121
+ // Handle comma as separator (only when not inside quotes)
122
+ if (char === ',' && !inDoubleQuotes && !inSingleQuotes) {
123
+ const trimmed = current.trim();
124
+ if (trimmed.length > 0) {
125
+ values.push(trimmed);
126
+ }
127
+ current = '';
128
+ i++;
129
+ continue;
130
+ }
131
+ // Regular character
132
+ current += char;
133
+ i++;
134
+ }
135
+ // Don't forget the last value
136
+ const trimmed = current.trim();
137
+ if (trimmed.length > 0) {
138
+ values.push(trimmed);
139
+ }
140
+ return values;
141
+ }
142
+ /**
143
+ * Format a field:value pair, quoting the value if necessary
144
+ */
145
+ formatFieldValue(field, value) {
146
+ // If the value is already quoted, use it as-is
147
+ if ((value.startsWith('"') && value.endsWith('"')) ||
148
+ (value.startsWith("'") && value.endsWith("'"))) {
149
+ return `${field}:${value}`;
150
+ }
151
+ // If value contains spaces or special characters, quote it
152
+ if (/\s|[():]/.test(value)) {
153
+ // Escape quotes within the value
154
+ const escapedValue = value.replace(/"/g, '\\"');
155
+ return `${field}:"${escapedValue}"`;
156
+ }
157
+ return `${field}:${value}`;
158
+ }
37
159
  /**
38
160
  * Validate a query string
39
161
  */
40
162
  validate(query) {
41
163
  try {
42
- const ast = (0, liqe_1.parse)(query);
164
+ const preprocessedQuery = this.preprocessQuery(query);
165
+ const ast = (0, liqe_1.parse)(preprocessedQuery);
43
166
  this.convertLiqeAst(ast);
44
167
  return true;
45
168
  }
@@ -72,10 +195,16 @@ class QueryParser {
72
195
  throw new QueryParseError('Invalid field or expression in Tag node');
73
196
  }
74
197
  const fieldName = this.normalizeFieldName(field.name);
198
+ // Handle RangeExpression (e.g., field:[min TO max])
199
+ if (expression.type === 'RangeExpression') {
200
+ return this.convertRangeExpression(fieldName, expression);
201
+ }
75
202
  const operator = this.convertLiqeOperator(tagNode.operator.operator);
76
203
  const value = this.convertLiqeValue(expression.value);
77
204
  // Check for wildcard patterns in string values
78
- if (operator === '==' && typeof value === 'string' && (value.includes('*') || value.includes('?'))) {
205
+ if (operator === '==' &&
206
+ typeof value === 'string' &&
207
+ (value.includes('*') || value.includes('?'))) {
79
208
  return this.createComparisonExpression(fieldName, 'LIKE', value);
80
209
  }
81
210
  return this.createComparisonExpression(fieldName, operator, value);
@@ -133,6 +262,31 @@ class QueryParser {
133
262
  value
134
263
  };
135
264
  }
265
+ /**
266
+ * Convert a Liqe RangeExpression to a QueryKit logical AND expression
267
+ * E.g., `field:[2 TO 5]` becomes `(field >= 2 AND field <= 5)`
268
+ */
269
+ convertRangeExpression(fieldName, expression) {
270
+ const range = expression.range;
271
+ // Handle null/undefined range values
272
+ if (range === null || range === undefined) {
273
+ throw new QueryParseError('Invalid range expression: missing range data');
274
+ }
275
+ const { min, max, minInclusive, maxInclusive } = range;
276
+ // Determine the operators based on inclusivity
277
+ const minOperator = minInclusive ? '>=' : '>';
278
+ const maxOperator = maxInclusive ? '<=' : '<';
279
+ // Create comparison expressions for min and max
280
+ const minComparison = this.createComparisonExpression(fieldName, minOperator, min);
281
+ const maxComparison = this.createComparisonExpression(fieldName, maxOperator, max);
282
+ // Combine with AND
283
+ return {
284
+ type: 'logical',
285
+ operator: 'AND',
286
+ left: minComparison,
287
+ right: maxComparison
288
+ };
289
+ }
136
290
  /**
137
291
  * Convert a Liqe operator to a QueryKit operator
138
292
  */
@@ -142,7 +296,9 @@ class QueryParser {
142
296
  return '==';
143
297
  }
144
298
  // Check if the operator is prefixed with a colon
145
- const actualOperator = operator.startsWith(':') ? operator.substring(1) : operator;
299
+ const actualOperator = operator.startsWith(':')
300
+ ? operator.substring(1)
301
+ : operator;
146
302
  // Map Liqe operators to QueryKit operators
147
303
  const operatorMap = {
148
304
  '=': '==',
@@ -151,7 +307,7 @@ class QueryParser {
151
307
  '>=': '>=',
152
308
  '<': '<',
153
309
  '<=': '<=',
154
- 'in': 'IN',
310
+ in: 'IN',
155
311
  'not in': 'NOT IN'
156
312
  };
157
313
  const queryKitOperator = operatorMap[actualOperator.toLowerCase()];
@@ -169,7 +325,9 @@ class QueryParser {
169
325
  if (value === null) {
170
326
  return null;
171
327
  }
172
- if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
328
+ if (typeof value === 'string' ||
329
+ typeof value === 'number' ||
330
+ typeof value === 'boolean') {
173
331
  return value;
174
332
  }
175
333
  if (Array.isArray(value)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gblikas/querykit",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A comprehensive query toolkit for TypeScript that simplifies building and executing data queries across different environments",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,5 +1,9 @@
1
1
  import { QueryParser, QueryParseError } from './parser';
2
- import { QueryExpression } from './types';
2
+ import {
3
+ IComparisonExpression,
4
+ ILogicalExpression,
5
+ QueryExpression
6
+ } from './types';
3
7
 
4
8
  // Replace the type definition with this approach
5
9
  type QueryParserPrivate = {
@@ -863,6 +867,210 @@ describe('QueryParser', () => {
863
867
 
864
868
  expect(parser.parse(query)).toEqual(expected);
865
869
  });
870
+
871
+ // IN operator syntax tests using consistent key:[values] pattern
872
+ describe('IN operator syntax (key:[values])', () => {
873
+ it('should parse "field:[val1, val2, val3]" bracket array syntax', () => {
874
+ const query = 'status:[todo, doing, done]';
875
+ const expected: QueryExpression = {
876
+ type: 'logical',
877
+ operator: 'OR',
878
+ left: {
879
+ type: 'logical',
880
+ operator: 'OR',
881
+ left: {
882
+ type: 'comparison',
883
+ field: 'status',
884
+ operator: '==',
885
+ value: 'todo'
886
+ },
887
+ right: {
888
+ type: 'comparison',
889
+ field: 'status',
890
+ operator: '==',
891
+ value: 'doing'
892
+ }
893
+ },
894
+ right: {
895
+ type: 'comparison',
896
+ field: 'status',
897
+ operator: '==',
898
+ value: 'done'
899
+ }
900
+ };
901
+
902
+ expect(parser.parse(query)).toEqual(expected);
903
+ });
904
+
905
+ it('should parse numeric values in bracket syntax', () => {
906
+ const query = 'id:[2, 3]';
907
+ const expected: QueryExpression = {
908
+ type: 'logical',
909
+ operator: 'OR',
910
+ left: {
911
+ type: 'comparison',
912
+ field: 'id',
913
+ operator: '==',
914
+ value: 2
915
+ },
916
+ right: {
917
+ type: 'comparison',
918
+ field: 'id',
919
+ operator: '==',
920
+ value: 3
921
+ }
922
+ };
923
+
924
+ expect(parser.parse(query)).toEqual(expected);
925
+ });
926
+
927
+ it('should parse single value in bracket syntax', () => {
928
+ const query = 'status:[active]';
929
+ const expected: QueryExpression = {
930
+ type: 'comparison',
931
+ field: 'status',
932
+ operator: '==',
933
+ value: 'active'
934
+ };
935
+
936
+ expect(parser.parse(query)).toEqual(expected);
937
+ });
938
+
939
+ it('should preserve range syntax "field:[min TO max]"', () => {
940
+ const query = 'id:[2 TO 5]';
941
+ const expected: QueryExpression = {
942
+ type: 'logical',
943
+ operator: 'AND',
944
+ left: {
945
+ type: 'comparison',
946
+ field: 'id',
947
+ operator: '>=',
948
+ value: 2
949
+ },
950
+ right: {
951
+ type: 'comparison',
952
+ field: 'id',
953
+ operator: '<=',
954
+ value: 5
955
+ }
956
+ };
957
+
958
+ expect(parser.parse(query)).toEqual(expected);
959
+ });
960
+
961
+ it('should parse exclusive range syntax "field:{min TO max}"', () => {
962
+ const query = 'id:{2 TO 5}';
963
+ const expected: QueryExpression = {
964
+ type: 'logical',
965
+ operator: 'AND',
966
+ left: {
967
+ type: 'comparison',
968
+ field: 'id',
969
+ operator: '>',
970
+ value: 2
971
+ },
972
+ right: {
973
+ type: 'comparison',
974
+ field: 'id',
975
+ operator: '<',
976
+ value: 5
977
+ }
978
+ };
979
+
980
+ expect(parser.parse(query)).toEqual(expected);
981
+ });
982
+
983
+ it('should parse bracket syntax combined with other expressions', () => {
984
+ const query = 'status:[todo, doing] AND priority:>2';
985
+ const parsed = parser.parse(query);
986
+
987
+ // Verify it's a logical AND at the top level
988
+ expect(parsed.type).toBe('logical');
989
+ expect((parsed as ILogicalExpression).operator).toBe('AND');
990
+
991
+ // Left side should be OR of status values
992
+ const left = (parsed as ILogicalExpression).left as ILogicalExpression;
993
+ expect(left.type).toBe('logical');
994
+ expect(left.operator).toBe('OR');
995
+ });
996
+
997
+ it('should handle values with spaces using quotes', () => {
998
+ const query = 'name:[John, "Jane Doe"]';
999
+ const parsed = parser.parse(query);
1000
+
1001
+ expect(parsed.type).toBe('logical');
1002
+ expect((parsed as ILogicalExpression).operator).toBe('OR');
1003
+
1004
+ const right = (parsed as ILogicalExpression)
1005
+ .right as IComparisonExpression;
1006
+ expect(right.value).toBe('Jane Doe');
1007
+ });
1008
+
1009
+ it('should validate bracket syntax queries', () => {
1010
+ expect(parser.validate('status:[todo, doing, done]')).toBe(true);
1011
+ expect(parser.validate('id:[1, 2, 3]')).toBe(true);
1012
+ expect(parser.validate('id:[1 TO 10]')).toBe(true);
1013
+ });
1014
+
1015
+ it('should handle mixed types in bracket syntax', () => {
1016
+ const query = 'priority:[1, 2, 3]';
1017
+ const parsed = parser.parse(query);
1018
+
1019
+ expect(parsed.type).toBe('logical');
1020
+ // All values should be numbers
1021
+ const getLeftmost = (expr: QueryExpression): IComparisonExpression => {
1022
+ if (expr.type === 'comparison') return expr;
1023
+ return getLeftmost((expr as ILogicalExpression).left);
1024
+ };
1025
+ expect(typeof getLeftmost(parsed).value).toBe('number');
1026
+ });
1027
+
1028
+ it('should handle quoted values with commas inside', () => {
1029
+ const query = 'name:["John, Jr.", "Jane"]';
1030
+ const parsed = parser.parse(query);
1031
+
1032
+ expect(parsed.type).toBe('logical');
1033
+ expect((parsed as ILogicalExpression).operator).toBe('OR');
1034
+
1035
+ const left = (parsed as ILogicalExpression)
1036
+ .left as IComparisonExpression;
1037
+ const right = (parsed as ILogicalExpression)
1038
+ .right as IComparisonExpression;
1039
+
1040
+ expect(left.value).toBe('John, Jr.');
1041
+ expect(right.value).toBe('Jane');
1042
+ });
1043
+
1044
+ it('should handle single-quoted values with commas inside', () => {
1045
+ const query = "name:['Hello, World', 'test']";
1046
+ const parsed = parser.parse(query);
1047
+
1048
+ expect(parsed.type).toBe('logical');
1049
+ const left = (parsed as ILogicalExpression)
1050
+ .left as IComparisonExpression;
1051
+ expect(left.value).toBe('Hello, World');
1052
+ });
1053
+
1054
+ it('should handle mixed quoted and unquoted values with commas', () => {
1055
+ const query = 'tags:["a,b,c", simple, "x,y"]';
1056
+ const parsed = parser.parse(query);
1057
+
1058
+ expect(parsed.type).toBe('logical');
1059
+ // Should have 3 values: "a,b,c", "simple", "x,y"
1060
+ const getValues = (expr: QueryExpression): string[] => {
1061
+ if (expr.type === 'comparison') {
1062
+ return [String(expr.value)];
1063
+ }
1064
+ const logical = expr as ILogicalExpression;
1065
+ return [...getValues(logical.left), ...getValues(logical.right!)];
1066
+ };
1067
+
1068
+ const values = getValues(parsed);
1069
+ expect(values).toContain('a,b,c');
1070
+ expect(values).toContain('simple');
1071
+ expect(values).toContain('x,y');
1072
+ });
1073
+ });
866
1074
  });
867
1075
 
868
1076
  describe('validate', () => {
@@ -7,6 +7,7 @@ import type {
7
7
  LiqeQuery,
8
8
  LogicalExpressionToken,
9
9
  ParenthesizedExpressionToken,
10
+ RangeExpressionToken,
10
11
  TagToken,
11
12
  UnaryOperatorToken
12
13
  } from 'liqe';
@@ -48,7 +49,9 @@ export class QueryParser implements IQueryParser {
48
49
  */
49
50
  public parse(query: string): QueryExpression {
50
51
  try {
51
- const liqeAst = liqeParse(query);
52
+ // Pre-process the query to handle IN operator syntax
53
+ const preprocessedQuery = this.preprocessQuery(query);
54
+ const liqeAst = liqeParse(preprocessedQuery);
52
55
  return this.convertLiqeAst(liqeAst);
53
56
  } catch (error) {
54
57
  throw new QueryParseError(
@@ -57,12 +60,155 @@ export class QueryParser implements IQueryParser {
57
60
  }
58
61
  }
59
62
 
63
+ /**
64
+ * Pre-process a query string to convert non-standard syntax to Liqe-compatible syntax.
65
+ * Supports:
66
+ * - `field:[val1, val2, val3]` → `(field:val1 OR field:val2 OR field:val3)`
67
+ *
68
+ * This keeps the syntax consistent with the `key:value` pattern used throughout QueryKit:
69
+ * - `priority:>2` (comparison)
70
+ * - `status:active` (equality)
71
+ * - `status:[todo, doing, done]` (IN / multiple values)
72
+ */
73
+ private preprocessQuery(query: string): string {
74
+ let result = query;
75
+
76
+ // Handle `field:[val1, val2, ...]` syntax (array-like, not range)
77
+ // Pattern: fieldName:[value1, value2, ...]
78
+ // We distinguish from range by checking for commas without "TO"
79
+ const bracketArrayPattern = /(\w+):\[([^\]]+)\]/g;
80
+ result = result.replace(bracketArrayPattern, (fullMatch, field, values) => {
81
+ // Check if this looks like a range expression (contains " TO ")
82
+ if (/\s+TO\s+/i.test(values)) {
83
+ // This is a range expression, keep as-is
84
+ return fullMatch;
85
+ }
86
+ // This is an array-like expression, convert to OR
87
+ return this.convertToOrExpression(field, values);
88
+ });
89
+
90
+ return result;
91
+ }
92
+
93
+ /**
94
+ * Convert a field and comma-separated values to an OR expression string
95
+ */
96
+ private convertToOrExpression(field: string, valuesStr: string): string {
97
+ // Parse values respecting quoted strings (commas inside quotes are preserved)
98
+ const values = this.parseCommaSeparatedValues(valuesStr);
99
+
100
+ if (values.length === 0) {
101
+ return `${field}:""`;
102
+ }
103
+
104
+ if (values.length === 1) {
105
+ return this.formatFieldValue(field, values[0]);
106
+ }
107
+
108
+ // Build OR expression: (field:val1 OR field:val2 OR ...)
109
+ const orClauses = values.map((v: string) =>
110
+ this.formatFieldValue(field, v)
111
+ );
112
+ return `(${orClauses.join(' OR ')})`;
113
+ }
114
+
115
+ /**
116
+ * Parse a comma-separated string into values, respecting quoted strings.
117
+ * Commas inside quoted strings are preserved as part of the value.
118
+ *
119
+ * Examples:
120
+ * - `a, b, c` → ['a', 'b', 'c']
121
+ * - `"John, Jr.", Jane` → ['"John, Jr."', 'Jane']
122
+ * - `'hello, world', test` → ["'hello, world'", 'test']
123
+ */
124
+ private parseCommaSeparatedValues(input: string): string[] {
125
+ const values: string[] = [];
126
+ let current = '';
127
+ let inDoubleQuotes = false;
128
+ let inSingleQuotes = false;
129
+ let i = 0;
130
+
131
+ while (i < input.length) {
132
+ const char = input[i];
133
+ const nextChar = input[i + 1];
134
+
135
+ // Handle escape sequences inside quotes
136
+ if ((inDoubleQuotes || inSingleQuotes) && char === '\\' && nextChar) {
137
+ // Include both the backslash and the escaped character
138
+ current += char + nextChar;
139
+ i += 2;
140
+ continue;
141
+ }
142
+
143
+ // Toggle double quote state
144
+ if (char === '"' && !inSingleQuotes) {
145
+ inDoubleQuotes = !inDoubleQuotes;
146
+ current += char;
147
+ i++;
148
+ continue;
149
+ }
150
+
151
+ // Toggle single quote state
152
+ if (char === "'" && !inDoubleQuotes) {
153
+ inSingleQuotes = !inSingleQuotes;
154
+ current += char;
155
+ i++;
156
+ continue;
157
+ }
158
+
159
+ // Handle comma as separator (only when not inside quotes)
160
+ if (char === ',' && !inDoubleQuotes && !inSingleQuotes) {
161
+ const trimmed = current.trim();
162
+ if (trimmed.length > 0) {
163
+ values.push(trimmed);
164
+ }
165
+ current = '';
166
+ i++;
167
+ continue;
168
+ }
169
+
170
+ // Regular character
171
+ current += char;
172
+ i++;
173
+ }
174
+
175
+ // Don't forget the last value
176
+ const trimmed = current.trim();
177
+ if (trimmed.length > 0) {
178
+ values.push(trimmed);
179
+ }
180
+
181
+ return values;
182
+ }
183
+
184
+ /**
185
+ * Format a field:value pair, quoting the value if necessary
186
+ */
187
+ private formatFieldValue(field: string, value: string): string {
188
+ // If the value is already quoted, use it as-is
189
+ if (
190
+ (value.startsWith('"') && value.endsWith('"')) ||
191
+ (value.startsWith("'") && value.endsWith("'"))
192
+ ) {
193
+ return `${field}:${value}`;
194
+ }
195
+
196
+ // If value contains spaces or special characters, quote it
197
+ if (/\s|[():]/.test(value)) {
198
+ // Escape quotes within the value
199
+ const escapedValue = value.replace(/"/g, '\\"');
200
+ return `${field}:"${escapedValue}"`;
201
+ }
202
+ return `${field}:${value}`;
203
+ }
204
+
60
205
  /**
61
206
  * Validate a query string
62
207
  */
63
208
  public validate(query: string): boolean {
64
209
  try {
65
- const ast = liqeParse(query);
210
+ const preprocessedQuery = this.preprocessQuery(query);
211
+ const ast = liqeParse(preprocessedQuery);
66
212
  this.convertLiqeAst(ast);
67
213
  return true;
68
214
  } catch {
@@ -81,7 +227,11 @@ export class QueryParser implements IQueryParser {
81
227
  switch (node.type) {
82
228
  case 'LogicalExpression': {
83
229
  const logicalNode = node as LogicalExpressionToken;
84
- const operator = (logicalNode.operator as BooleanOperatorToken | ImplicitBooleanOperatorToken).operator;
230
+ const operator = (
231
+ logicalNode.operator as
232
+ | BooleanOperatorToken
233
+ | ImplicitBooleanOperatorToken
234
+ ).operator;
85
235
  return this.createLogicalExpression(
86
236
  this.convertLogicalOperator(operator),
87
237
  logicalNode.left,
@@ -97,30 +247,37 @@ export class QueryParser implements IQueryParser {
97
247
  case 'Tag': {
98
248
  const tagNode = node as TagToken;
99
249
  const field = tagNode.field as FieldToken;
100
- const expression = tagNode.expression as ExpressionToken & { value: QueryValue };
101
-
250
+ const expression = tagNode.expression as ExpressionToken & {
251
+ value: QueryValue;
252
+ };
253
+
102
254
  if (!field || !expression) {
103
255
  throw new QueryParseError('Invalid field or expression in Tag node');
104
256
  }
105
257
 
106
258
  const fieldName = this.normalizeFieldName(field.name);
259
+
260
+ // Handle RangeExpression (e.g., field:[min TO max])
261
+ if (expression.type === 'RangeExpression') {
262
+ return this.convertRangeExpression(
263
+ fieldName,
264
+ expression as unknown as RangeExpressionToken
265
+ );
266
+ }
267
+
107
268
  const operator = this.convertLiqeOperator(tagNode.operator.operator);
108
269
  const value = this.convertLiqeValue(expression.value);
109
270
 
110
271
  // Check for wildcard patterns in string values
111
- if (operator === '==' && typeof value === 'string' && (value.includes('*') || value.includes('?'))) {
112
- return this.createComparisonExpression(
113
- fieldName,
114
- 'LIKE',
115
- value
116
- );
272
+ if (
273
+ operator === '==' &&
274
+ typeof value === 'string' &&
275
+ (value.includes('*') || value.includes('?'))
276
+ ) {
277
+ return this.createComparisonExpression(fieldName, 'LIKE', value);
117
278
  }
118
279
 
119
- return this.createComparisonExpression(
120
- fieldName,
121
- operator,
122
- value
123
- );
280
+ return this.createComparisonExpression(fieldName, operator, value);
124
281
  }
125
282
 
126
283
  case 'EmptyExpression':
@@ -138,7 +295,9 @@ export class QueryParser implements IQueryParser {
138
295
  }
139
296
 
140
297
  default:
141
- throw new QueryParseError(`Unsupported node type: ${(node as { type: string }).type}`);
298
+ throw new QueryParseError(
299
+ `Unsupported node type: ${(node as { type: string }).type}`
300
+ );
142
301
  }
143
302
  }
144
303
 
@@ -190,6 +349,48 @@ export class QueryParser implements IQueryParser {
190
349
  };
191
350
  }
192
351
 
352
+ /**
353
+ * Convert a Liqe RangeExpression to a QueryKit logical AND expression
354
+ * E.g., `field:[2 TO 5]` becomes `(field >= 2 AND field <= 5)`
355
+ */
356
+ private convertRangeExpression(
357
+ fieldName: string,
358
+ expression: RangeExpressionToken
359
+ ): QueryExpression {
360
+ const range = expression.range;
361
+
362
+ // Handle null/undefined range values
363
+ if (range === null || range === undefined) {
364
+ throw new QueryParseError('Invalid range expression: missing range data');
365
+ }
366
+
367
+ const { min, max, minInclusive, maxInclusive } = range;
368
+
369
+ // Determine the operators based on inclusivity
370
+ const minOperator: ComparisonOperator = minInclusive ? '>=' : '>';
371
+ const maxOperator: ComparisonOperator = maxInclusive ? '<=' : '<';
372
+
373
+ // Create comparison expressions for min and max
374
+ const minComparison = this.createComparisonExpression(
375
+ fieldName,
376
+ minOperator,
377
+ min
378
+ );
379
+ const maxComparison = this.createComparisonExpression(
380
+ fieldName,
381
+ maxOperator,
382
+ max
383
+ );
384
+
385
+ // Combine with AND
386
+ return {
387
+ type: 'logical',
388
+ operator: 'AND',
389
+ left: minComparison,
390
+ right: maxComparison
391
+ };
392
+ }
393
+
193
394
  /**
194
395
  * Convert a Liqe operator to a QueryKit operator
195
396
  */
@@ -200,7 +401,9 @@ export class QueryParser implements IQueryParser {
200
401
  }
201
402
 
202
403
  // Check if the operator is prefixed with a colon
203
- const actualOperator = operator.startsWith(':') ? operator.substring(1) : operator;
404
+ const actualOperator = operator.startsWith(':')
405
+ ? operator.substring(1)
406
+ : operator;
204
407
 
205
408
  // Map Liqe operators to QueryKit operators
206
409
  const operatorMap: Record<string, ComparisonOperator> = {
@@ -210,7 +413,7 @@ export class QueryParser implements IQueryParser {
210
413
  '>=': '>=',
211
414
  '<': '<',
212
415
  '<=': '<=',
213
- 'in': 'IN',
416
+ in: 'IN',
214
417
  'not in': 'NOT IN'
215
418
  };
216
419
 
@@ -231,11 +434,15 @@ export class QueryParser implements IQueryParser {
231
434
  if (value === null) {
232
435
  return null;
233
436
  }
234
-
235
- if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
437
+
438
+ if (
439
+ typeof value === 'string' ||
440
+ typeof value === 'number' ||
441
+ typeof value === 'boolean'
442
+ ) {
236
443
  return value as QueryValue;
237
444
  }
238
-
445
+
239
446
  if (Array.isArray(value)) {
240
447
  // Security fix: Recursively validate array elements
241
448
  const validatedArray = value.map(item => {
@@ -246,10 +453,12 @@ export class QueryParser implements IQueryParser {
246
453
  });
247
454
  return validatedArray as QueryValue;
248
455
  }
249
-
456
+
250
457
  // Security fix: Reject all object types to prevent NoSQL injection
251
458
  if (typeof value === 'object') {
252
- throw new QueryParseError('Object values are not supported for security reasons');
459
+ throw new QueryParseError(
460
+ 'Object values are not supported for security reasons'
461
+ );
253
462
  }
254
463
 
255
464
  throw new QueryParseError(`Unsupported value type: ${typeof value}`);
@@ -265,4 +474,4 @@ export class QueryParser implements IQueryParser {
265
474
 
266
475
  return this.options.fieldMappings[normalizedField] ?? normalizedField;
267
476
  }
268
- }
477
+ }