search-input-query-parser 0.1.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.
Files changed (58) hide show
  1. package/dist/cjs/first-pass-parser.js +77 -0
  2. package/dist/cjs/lexer.js +322 -0
  3. package/dist/cjs/parse-in-values.js +65 -0
  4. package/dist/cjs/parse-primary.js +154 -0
  5. package/dist/cjs/parse-range-expression.js +174 -0
  6. package/dist/cjs/parser.js +85 -0
  7. package/dist/cjs/search-query-to-sql.js +346 -0
  8. package/dist/cjs/transform-to-expression.js +130 -0
  9. package/dist/cjs/validate-expression-fields.js +244 -0
  10. package/dist/cjs/validate-in-expression.js +33 -0
  11. package/dist/cjs/validate-string.js +65 -0
  12. package/dist/cjs/validate-wildcard.js +40 -0
  13. package/dist/cjs/validator.js +34 -0
  14. package/dist/esm/first-pass-parser.js +73 -0
  15. package/dist/esm/lexer.js +315 -0
  16. package/dist/esm/parse-in-values.js +61 -0
  17. package/dist/esm/parse-primary.js +147 -0
  18. package/dist/esm/parse-range-expression.js +170 -0
  19. package/dist/esm/parser.js +81 -0
  20. package/dist/esm/search-query-to-sql.js +341 -0
  21. package/dist/esm/transform-to-expression.js +126 -0
  22. package/dist/esm/validate-expression-fields.js +240 -0
  23. package/dist/esm/validate-in-expression.js +29 -0
  24. package/dist/esm/validate-string.js +61 -0
  25. package/dist/esm/validate-wildcard.js +36 -0
  26. package/dist/esm/validator.js +30 -0
  27. package/dist/types/first-pass-parser.d.ts +40 -0
  28. package/dist/types/lexer.d.ts +27 -0
  29. package/dist/types/parse-in-values.d.ts +3 -0
  30. package/dist/types/parse-primary.d.ts +6 -0
  31. package/dist/types/parse-range-expression.d.ts +2 -0
  32. package/dist/types/parser.d.ts +68 -0
  33. package/dist/types/search-query-to-sql.d.ts +18 -0
  34. package/dist/types/transform-to-expression.d.ts +3 -0
  35. package/dist/types/validate-expression-fields.d.ts +4 -0
  36. package/dist/types/validate-in-expression.d.ts +3 -0
  37. package/dist/types/validate-string.d.ts +3 -0
  38. package/dist/types/validate-wildcard.d.ts +3 -0
  39. package/dist/types/validator.d.ts +8 -0
  40. package/package.json +52 -0
  41. package/src/first-pass-parser.test.ts +441 -0
  42. package/src/first-pass-parser.ts +144 -0
  43. package/src/lexer.test.ts +439 -0
  44. package/src/lexer.ts +387 -0
  45. package/src/parse-in-values.ts +74 -0
  46. package/src/parse-primary.ts +179 -0
  47. package/src/parse-range-expression.ts +187 -0
  48. package/src/parser.test.ts +982 -0
  49. package/src/parser.ts +219 -0
  50. package/src/search-query-to-sql.test.ts +503 -0
  51. package/src/search-query-to-sql.ts +506 -0
  52. package/src/transform-to-expression.ts +153 -0
  53. package/src/validate-expression-fields.ts +296 -0
  54. package/src/validate-in-expression.ts +36 -0
  55. package/src/validate-string.ts +73 -0
  56. package/src/validate-wildcard.ts +45 -0
  57. package/src/validator.test.ts +192 -0
  58. package/src/validator.ts +53 -0
@@ -0,0 +1,296 @@
1
+ import { FieldSchema } from "./parser";
2
+ import { FirstPassExpression } from "./first-pass-parser";
3
+ import { ValidationError } from "./validator";
4
+
5
+ // Helper to validate numeric values
6
+ const validateNumber = (
7
+ value: string,
8
+ position: number,
9
+ errors: ValidationError[]
10
+ ): boolean => {
11
+ if (value === "") return false;
12
+ if (isNaN(Number(value))) {
13
+ errors.push({
14
+ message: "Invalid numeric value",
15
+ position,
16
+ length: value.length,
17
+ });
18
+ return false;
19
+ }
20
+ return true;
21
+ };
22
+
23
+ // Helper to validate range values for numeric fields
24
+ const validateNumericRange = (
25
+ start: string,
26
+ end: string,
27
+ basePosition: number,
28
+ errors: ValidationError[]
29
+ ): boolean => {
30
+ let isValid = true;
31
+ const startPos = basePosition;
32
+ const endPos = basePosition + start.length + 2; // +2 for the '..'
33
+
34
+ if (start && !validateNumber(start, startPos, errors)) {
35
+ isValid = false;
36
+ }
37
+ if (end && !validateNumber(end, endPos, errors)) {
38
+ isValid = false;
39
+ }
40
+
41
+ if (isValid && start && end) {
42
+ const startNum = Number(start);
43
+ const endNum = Number(end);
44
+ if (startNum > endNum) {
45
+ errors.push({
46
+ message: "Range start must be less than or equal to range end",
47
+ position: basePosition,
48
+ length: start.length + 2 + end.length,
49
+ });
50
+ isValid = false;
51
+ }
52
+ }
53
+
54
+ return isValid;
55
+ };
56
+
57
+ // Helper to validate numeric comparison operators
58
+ const validateNumericComparison = (
59
+ operator: string,
60
+ value: string,
61
+ basePosition: number,
62
+ errors: ValidationError[]
63
+ ): boolean => {
64
+ const valuePosition = basePosition + operator.length;
65
+ return validateNumber(value, valuePosition, errors);
66
+ };
67
+
68
+ // Field validation helpers
69
+ const validateFieldValue = (
70
+ expr: FirstPassExpression,
71
+ allowedFields: Set<string>,
72
+ errors: ValidationError[],
73
+ schemas: Map<string, FieldSchema>
74
+ ): void => {
75
+ switch (expr.type) {
76
+ case "IN": {
77
+ if (!allowedFields.has(expr.field.toLowerCase())) {
78
+ errors.push({
79
+ message: `Invalid field: "${expr.field}"`,
80
+ position: expr.position,
81
+ length: expr.field.length,
82
+ });
83
+ }
84
+
85
+ // Get schema for type validation
86
+ const schema = schemas.get(expr.field.toLowerCase());
87
+ if (schema) {
88
+ expr.values.forEach((value, index) => {
89
+ switch (schema.type) {
90
+ case "number":
91
+ if (isNaN(Number(value))) {
92
+ errors.push({
93
+ message: "Invalid numeric value",
94
+ position:
95
+ expr.position +
96
+ expr.field.length +
97
+ 4 +
98
+ index * (value.length + 1),
99
+ length: value.length,
100
+ });
101
+ }
102
+ break;
103
+ case "date":
104
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
105
+ errors.push({
106
+ message: "Invalid date format",
107
+ position:
108
+ expr.position +
109
+ expr.field.length +
110
+ 3 +
111
+ index * (value.length + 1),
112
+ length: value.length,
113
+ });
114
+ }
115
+ break;
116
+ }
117
+ });
118
+ }
119
+ break;
120
+ }
121
+
122
+ case "WILDCARD": {
123
+ // For wildcard patterns, validate against field type constraints
124
+ const schema = schemas.get(expr.prefix.toLowerCase());
125
+ if (schema?.type === "number" || schema?.type === "date") {
126
+ errors.push({
127
+ message: `Wildcards are not allowed for ${schema.type} fields`,
128
+ position: expr.position,
129
+ length: expr.length,
130
+ });
131
+ }
132
+ break;
133
+ }
134
+
135
+ case "STRING": {
136
+ const colonIndex = expr.value.indexOf(":");
137
+ if (colonIndex === -1) return;
138
+
139
+ const fieldName = expr.value.substring(0, colonIndex).trim();
140
+ const value = expr.value.substring(colonIndex + 1).trim();
141
+
142
+ if (!allowedFields.has(fieldName.toLowerCase()) && colonIndex > 0) {
143
+ errors.push({
144
+ message: `Invalid field: "${fieldName}"`,
145
+ position: expr.position,
146
+ length: colonIndex,
147
+ });
148
+ }
149
+
150
+ if (!value) {
151
+ errors.push({
152
+ message: "Expected field value",
153
+ position: expr.position,
154
+ length: colonIndex + 1,
155
+ });
156
+ return;
157
+ }
158
+
159
+ if (value.startsWith(":")) {
160
+ errors.push({
161
+ message: "Missing field name",
162
+ position: expr.position,
163
+ length: value.length + colonIndex + 1,
164
+ });
165
+ return;
166
+ }
167
+
168
+ const schema = schemas.get(fieldName.toLowerCase());
169
+ if (!schema) return;
170
+
171
+ const valueStartPosition = expr.position + colonIndex + 1;
172
+
173
+ if (schema.type === "number") {
174
+ if (value.includes("..")) {
175
+ if (value === ".." || value.includes("...")) {
176
+ errors.push({
177
+ message: "Invalid range format",
178
+ position: valueStartPosition,
179
+ length: value.length,
180
+ });
181
+ return;
182
+ }
183
+
184
+ const [start, end] = value.split("..");
185
+ validateNumericRange(start, end, valueStartPosition, errors);
186
+ return;
187
+ }
188
+
189
+ const comparisonMatch = value.match(/^(>=|>|<=|<)(.*)$/);
190
+ if (comparisonMatch) {
191
+ const [, operator, compValue] = comparisonMatch;
192
+
193
+ const invalidOp = /^[<>]{2,}|>=>/;
194
+ if (invalidOp.test(value)) {
195
+ errors.push({
196
+ message: "Invalid range operator",
197
+ position: valueStartPosition,
198
+ length: 3,
199
+ });
200
+ return;
201
+ }
202
+
203
+ if (!compValue) {
204
+ errors.push({
205
+ message: "Expected range value",
206
+ position: valueStartPosition + operator.length,
207
+ length: 0,
208
+ });
209
+ return;
210
+ }
211
+
212
+ validateNumericComparison(
213
+ operator,
214
+ compValue,
215
+ valueStartPosition,
216
+ errors
217
+ );
218
+ return;
219
+ }
220
+
221
+ validateNumber(value, valueStartPosition, errors);
222
+ return;
223
+ }
224
+
225
+ if (schema.type === "date") {
226
+ const dateValidator = (dateStr: string) => {
227
+ if (!dateStr) return true;
228
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
229
+ return false;
230
+ }
231
+ const date = new Date(dateStr);
232
+ return (
233
+ !isNaN(date.getTime()) &&
234
+ dateStr === date.toISOString().split("T")[0]
235
+ );
236
+ };
237
+
238
+ if (value.includes("..")) {
239
+ const [start, end] = value.split("..");
240
+ if (!dateValidator(start) || !dateValidator(end)) {
241
+ errors.push({
242
+ message: "Invalid date format",
243
+ position: valueStartPosition,
244
+ length: value.length,
245
+ });
246
+ return;
247
+ }
248
+ } else {
249
+ const comparisonMatch = value.match(/^(>=|>|<=|<)(.*)$/);
250
+ if (comparisonMatch) {
251
+ const [, , dateStr] = comparisonMatch;
252
+ if (!dateValidator(dateStr)) {
253
+ errors.push({
254
+ message: "Invalid date format",
255
+ position: valueStartPosition,
256
+ length: value.length,
257
+ });
258
+ return;
259
+ }
260
+ } else if (!dateValidator(value)) {
261
+ errors.push({
262
+ message: "Invalid date format",
263
+ position: valueStartPosition,
264
+ length: value.length,
265
+ });
266
+ return;
267
+ }
268
+ }
269
+ }
270
+ break;
271
+ }
272
+ }
273
+ };
274
+
275
+ export const validateExpressionFields = (
276
+ expr: FirstPassExpression,
277
+ allowedFields: Set<string>,
278
+ errors: ValidationError[],
279
+ schemas: Map<string, FieldSchema>
280
+ ): void => {
281
+ switch (expr.type) {
282
+ case "STRING":
283
+ case "WILDCARD":
284
+ case "IN":
285
+ validateFieldValue(expr, allowedFields, errors, schemas);
286
+ break;
287
+ case "AND":
288
+ case "OR":
289
+ validateExpressionFields(expr.left, allowedFields, errors, schemas);
290
+ validateExpressionFields(expr.right, allowedFields, errors, schemas);
291
+ break;
292
+ case "NOT":
293
+ validateExpressionFields(expr.expression, allowedFields, errors, schemas);
294
+ break;
295
+ }
296
+ };
@@ -0,0 +1,36 @@
1
+ import { InExpression } from "./first-pass-parser";
2
+ import { ValidationError, reservedWords } from "./validator";
3
+
4
+ export const validateInExpression = (
5
+ expr: InExpression,
6
+ errors: ValidationError[]
7
+ ): void => {
8
+ // Validate field name pattern
9
+ if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(expr.field)) {
10
+ errors.push({
11
+ message: "Invalid characters in field name",
12
+ position: expr.position,
13
+ length: expr.field.length,
14
+ });
15
+ }
16
+
17
+ // Check for reserved words
18
+ if (reservedWords.has(expr.field.toUpperCase())) {
19
+ errors.push({
20
+ message: `${expr.field} is a reserved word`,
21
+ position: expr.position,
22
+ length: expr.field.length,
23
+ });
24
+ }
25
+
26
+ // Validate value format based on field type
27
+ expr.values.forEach((value, index) => {
28
+ if (value.includes(",")) {
29
+ errors.push({
30
+ message: "Invalid character in IN value",
31
+ position: expr.position + expr.field.length + 3 + index * (value.length + 1),
32
+ length: value.length,
33
+ });
34
+ }
35
+ });
36
+ };
@@ -0,0 +1,73 @@
1
+ import { StringLiteral, WildcardPattern } from "./first-pass-parser";
2
+ import { ValidationError, reservedWords } from "./validator";
3
+ import { validateWildcard } from "./validate-wildcard";
4
+
5
+ // Validate individual strings (field:value pairs or plain terms)
6
+ export const validateString = (
7
+ expr: StringLiteral | WildcardPattern,
8
+ errors: ValidationError[]
9
+ ) => {
10
+ // Validate wildcard usage
11
+ validateWildcard(expr, errors);
12
+
13
+ // For wildcard patterns, no additional validation needed
14
+ if (expr.type === "WILDCARD") {
15
+ return;
16
+ }
17
+
18
+ // Handle STRING type
19
+ // Check for empty field values
20
+ if (expr.value.endsWith(":")) {
21
+ errors.push({
22
+ message: "Expected field value",
23
+ position: expr.position,
24
+ length: expr.length,
25
+ });
26
+ return;
27
+ }
28
+
29
+ // Check for field values that start with colon
30
+ if (expr.value.startsWith(":")) {
31
+ errors.push({
32
+ message: "Missing field name",
33
+ position: expr.position,
34
+ length: expr.length,
35
+ });
36
+ return;
37
+ }
38
+
39
+ // For field:value patterns, validate the field name
40
+ if (expr.value.includes(":")) {
41
+ const [fieldName] = expr.value.split(":");
42
+
43
+ // Check for reserved words used as field names
44
+ if (reservedWords.has(fieldName.toUpperCase())) {
45
+ errors.push({
46
+ message: `${fieldName} is a reserved word`,
47
+ position: expr.position,
48
+ length: fieldName.length,
49
+ });
50
+ return;
51
+ }
52
+
53
+ // Check for invalid characters in field names
54
+ if (!/^[a-zA-Z0-9_-]+$/.test(fieldName)) {
55
+ errors.push({
56
+ message: "Invalid characters in field name",
57
+ position: expr.position,
58
+ length: fieldName.length,
59
+ });
60
+ return;
61
+ }
62
+ }
63
+
64
+ // Handle standalone reserved words (not in field:value pattern)
65
+ if (!expr.value.includes(":") &&
66
+ reservedWords.has(expr.value.toUpperCase())) {
67
+ errors.push({
68
+ message: `${expr.value} is a reserved word`,
69
+ position: expr.position,
70
+ length: expr.length,
71
+ });
72
+ }
73
+ };
@@ -0,0 +1,45 @@
1
+ import { StringLiteral, WildcardPattern } from "./first-pass-parser";
2
+ import { ValidationError } from "./validator";
3
+
4
+ // Validates wildcard patterns
5
+
6
+ export const validateWildcard = (
7
+ expr: StringLiteral | WildcardPattern,
8
+ errors: ValidationError[]
9
+ ) => {
10
+ const value = expr.type === "STRING" ? expr.value : expr.prefix + "*";
11
+ const starCount = (value.match(/\*/g) || []).length;
12
+ const isQuoted = expr.quoted;
13
+
14
+ // For unquoted strings
15
+ if (!isQuoted) {
16
+ const firstStar = value.indexOf("*");
17
+ if (starCount > 1) {
18
+ const secondStar = value.indexOf("*", firstStar + 1);
19
+ errors.push({
20
+ message: "Only one trailing wildcard (*) is allowed",
21
+ position: expr.position + secondStar,
22
+ length: 1,
23
+ });
24
+ }
25
+ if ((firstStar !== -1 && firstStar !== value.length - 1) && !value.endsWith("**")) {
26
+ errors.push({
27
+ message: "Wildcard (*) can only appear at the end of a term",
28
+ position: expr.position + firstStar,
29
+ length: 1,
30
+ });
31
+ }
32
+ }
33
+
34
+ // For quoted strings
35
+ else {
36
+ // Handle multiple wildcards or internal wildcards in quoted strings
37
+ if (value.endsWith("**")) {
38
+ errors.push({
39
+ message: "Only one trailing wildcard (*) is allowed",
40
+ position: expr.position + value.length - 1,
41
+ length: 1,
42
+ });
43
+ }
44
+ }
45
+ };
@@ -0,0 +1,192 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import { validateSearchQuery, ValidationError } from "./validator";
3
+ import { tokenize, createStream } from "./lexer";
4
+ import { parseExpression } from "./first-pass-parser";
5
+
6
+ describe("Search Query Validator", () => {
7
+ const validateQuery = (input: string): ValidationError[] => {
8
+ const tokens = tokenize(input);
9
+ const stream = createStream(tokens);
10
+ const result = parseExpression(stream);
11
+ return validateSearchQuery(result.result);
12
+ };
13
+
14
+ describe("Field Name Validation", () => {
15
+ test("accepts valid field names", () => {
16
+ expect(validateQuery("simple:value")).toEqual([]);
17
+ expect(validateQuery("field123:value")).toEqual([]);
18
+ expect(validateQuery("field_name:value")).toEqual([]);
19
+ expect(validateQuery("field-name:value")).toEqual([]);
20
+ });
21
+
22
+ test("handles field names with special characters", () => {
23
+ expect(validateQuery("special@field:value")).toEqual([
24
+ {
25
+ message: "Invalid characters in field name",
26
+ position: 0,
27
+ length: 13,
28
+ },
29
+ ]);
30
+ });
31
+
32
+ test("validates multiple field:value pairs", () => {
33
+ expect(validateQuery("valid:value special!:value")).toEqual([
34
+ {
35
+ message: "Invalid characters in field name",
36
+ position: 12,
37
+ length: 8,
38
+ },
39
+ ]);
40
+ });
41
+ });
42
+
43
+ describe("Field Value Validation", () => {
44
+ test("detects missing field values", () => {
45
+ expect(validateQuery("field:")).toEqual([
46
+ {
47
+ message: "Expected field value",
48
+ position: 0,
49
+ length: 6,
50
+ },
51
+ ]);
52
+ });
53
+
54
+ test("validates field values with spaces", () => {
55
+ expect(validateQuery('field:"value with spaces"')).toEqual([]);
56
+ expect(validateQuery("field:value with spaces")).toEqual([]);
57
+ });
58
+
59
+ test("validates empty colon patterns", () => {
60
+ expect(validateQuery(":value")).toEqual([
61
+ {
62
+ message: "Missing field name",
63
+ position: 0,
64
+ length: 6,
65
+ },
66
+ ]);
67
+ });
68
+ });
69
+
70
+ describe("Reserved Word Validation", () => {
71
+ test("detects reserved words as field names", () => {
72
+ expect(validateQuery("AND:value")).toEqual([
73
+ {
74
+ message: "AND is a reserved word",
75
+ position: 0,
76
+ length: 3,
77
+ },
78
+ ]);
79
+
80
+ expect(validateQuery("OR:value")).toEqual([
81
+ {
82
+ message: "OR is a reserved word",
83
+ position: 0,
84
+ length: 2,
85
+ },
86
+ ]);
87
+ });
88
+
89
+ test("detects reserved words as standalone terms", () => {
90
+ // This will throw because it's handled by the parser
91
+ expect(() => validateQuery("AND")).toThrow("AND is a reserved word");
92
+ expect(() => validateQuery("OR")).toThrow("OR is a reserved word");
93
+ });
94
+
95
+ test("allows reserved words as field values", () => {
96
+ expect(validateQuery("field:AND")).toEqual([]);
97
+ expect(validateQuery("field:OR")).toEqual([]);
98
+ expect(validateQuery('field:"AND OR"')).toEqual([]);
99
+ });
100
+ });
101
+
102
+ describe("Complex Expression Validation", () => {
103
+ test("validates nested expressions", () => {
104
+ expect(validateQuery("(field:value AND invalid!:value)")).toEqual([
105
+ {
106
+ message: "Invalid characters in field name",
107
+ position: 17,
108
+ length: 8,
109
+ },
110
+ ]);
111
+ });
112
+
113
+ test("validates multiple errors in one expression", () => {
114
+ expect(validateQuery("AND:test OR invalid!:value")).toEqual([
115
+ {
116
+ message: "AND is a reserved word",
117
+ position: 0,
118
+ length: 3,
119
+ },
120
+ {
121
+ message: "Invalid characters in field name",
122
+ position: 12,
123
+ length: 8,
124
+ },
125
+ ]);
126
+ });
127
+
128
+ test("validates complex nested expressions", () => {
129
+ expect(
130
+ validateQuery("(field:value AND (OR:test OR valid:value))")
131
+ ).toEqual([
132
+ {
133
+ message: "OR is a reserved word",
134
+ position: 18,
135
+ length: 2,
136
+ },
137
+ ]);
138
+ });
139
+ });
140
+
141
+ describe("Edge Cases", () => {
142
+ test("handles empty input", () => {
143
+ expect(() => validateQuery("")).toThrow("Unexpected token");
144
+ });
145
+
146
+ test("handles whitespace-only input", () => {
147
+ expect(() => validateQuery(" ")).toThrow("Unexpected token");
148
+ });
149
+
150
+ test("invalidates consecutive colons", () => {
151
+ expect(validateQuery("field::value")).toEqual([
152
+ {
153
+ message: "Expected field value",
154
+ position: 0,
155
+ length: 6,
156
+ },
157
+ {
158
+ message: "Missing field name",
159
+ position: 6,
160
+ length: 6,
161
+ }
162
+ ]);
163
+ });
164
+
165
+ test("validates field names with only special characters", () => {
166
+ expect(validateQuery("@#$:value")).toEqual([
167
+ {
168
+ message: "Invalid characters in field name",
169
+ position: 0,
170
+ length: 3,
171
+ },
172
+ ]);
173
+ });
174
+
175
+ test("validates mixed valid and invalid patterns", () => {
176
+ const complexQuery =
177
+ 'valid:value AND field:"test" OR @invalid:value AND OR:test';
178
+ expect(validateQuery(complexQuery)).toEqual([
179
+ {
180
+ message: "Invalid characters in field name",
181
+ position: 32,
182
+ length: 8,
183
+ },
184
+ {
185
+ message: "OR is a reserved word",
186
+ position: 51,
187
+ length: 2,
188
+ },
189
+ ]);
190
+ });
191
+ });
192
+ });
@@ -0,0 +1,53 @@
1
+ import {
2
+ FirstPassExpression,
3
+ } from "./first-pass-parser";
4
+
5
+ import { FieldSchema } from "./parser";
6
+ import { validateInExpression } from "./validate-in-expression";
7
+ import { validateString } from "./validate-string";
8
+
9
+ // Validation error type
10
+ export type ValidationError = {
11
+ message: string;
12
+ position: number;
13
+ length: number;
14
+ };
15
+
16
+ export const reservedWords = new Set(["AND", "OR"]);
17
+
18
+ const walkExpression = (
19
+ expr: FirstPassExpression,
20
+ errors: ValidationError[]
21
+ ) => {
22
+ switch (expr.type) {
23
+ case "STRING":
24
+ case "WILDCARD":
25
+ validateString(expr, errors);
26
+ break;
27
+ case "AND":
28
+ case "OR":
29
+ walkExpression(expr.left, errors);
30
+ walkExpression(expr.right, errors);
31
+ break;
32
+ case "NOT":
33
+ walkExpression(expr.expression, errors);
34
+ break;
35
+ case "IN":
36
+ validateInExpression(expr, errors);
37
+ break;
38
+ }
39
+ };
40
+
41
+ export const validateSearchQuery = (
42
+ expression: FirstPassExpression
43
+ ): ValidationError[] => {
44
+ const errors: ValidationError[] = [];
45
+
46
+ if (expression === null) {
47
+ return errors;
48
+ }
49
+
50
+ walkExpression(expression, errors);
51
+
52
+ return errors;
53
+ };