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.
- package/dist/cjs/first-pass-parser.js +77 -0
- package/dist/cjs/lexer.js +322 -0
- package/dist/cjs/parse-in-values.js +65 -0
- package/dist/cjs/parse-primary.js +154 -0
- package/dist/cjs/parse-range-expression.js +174 -0
- package/dist/cjs/parser.js +85 -0
- package/dist/cjs/search-query-to-sql.js +346 -0
- package/dist/cjs/transform-to-expression.js +130 -0
- package/dist/cjs/validate-expression-fields.js +244 -0
- package/dist/cjs/validate-in-expression.js +33 -0
- package/dist/cjs/validate-string.js +65 -0
- package/dist/cjs/validate-wildcard.js +40 -0
- package/dist/cjs/validator.js +34 -0
- package/dist/esm/first-pass-parser.js +73 -0
- package/dist/esm/lexer.js +315 -0
- package/dist/esm/parse-in-values.js +61 -0
- package/dist/esm/parse-primary.js +147 -0
- package/dist/esm/parse-range-expression.js +170 -0
- package/dist/esm/parser.js +81 -0
- package/dist/esm/search-query-to-sql.js +341 -0
- package/dist/esm/transform-to-expression.js +126 -0
- package/dist/esm/validate-expression-fields.js +240 -0
- package/dist/esm/validate-in-expression.js +29 -0
- package/dist/esm/validate-string.js +61 -0
- package/dist/esm/validate-wildcard.js +36 -0
- package/dist/esm/validator.js +30 -0
- package/dist/types/first-pass-parser.d.ts +40 -0
- package/dist/types/lexer.d.ts +27 -0
- package/dist/types/parse-in-values.d.ts +3 -0
- package/dist/types/parse-primary.d.ts +6 -0
- package/dist/types/parse-range-expression.d.ts +2 -0
- package/dist/types/parser.d.ts +68 -0
- package/dist/types/search-query-to-sql.d.ts +18 -0
- package/dist/types/transform-to-expression.d.ts +3 -0
- package/dist/types/validate-expression-fields.d.ts +4 -0
- package/dist/types/validate-in-expression.d.ts +3 -0
- package/dist/types/validate-string.d.ts +3 -0
- package/dist/types/validate-wildcard.d.ts +3 -0
- package/dist/types/validator.d.ts +8 -0
- package/package.json +52 -0
- package/src/first-pass-parser.test.ts +441 -0
- package/src/first-pass-parser.ts +144 -0
- package/src/lexer.test.ts +439 -0
- package/src/lexer.ts +387 -0
- package/src/parse-in-values.ts +74 -0
- package/src/parse-primary.ts +179 -0
- package/src/parse-range-expression.ts +187 -0
- package/src/parser.test.ts +982 -0
- package/src/parser.ts +219 -0
- package/src/search-query-to-sql.test.ts +503 -0
- package/src/search-query-to-sql.ts +506 -0
- package/src/transform-to-expression.ts +153 -0
- package/src/validate-expression-fields.ts +296 -0
- package/src/validate-in-expression.ts +36 -0
- package/src/validate-string.ts +73 -0
- package/src/validate-wildcard.ts +45 -0
- package/src/validator.test.ts +192 -0
- package/src/validator.ts +53 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.transformToExpression = void 0;
|
|
4
|
+
const parse_range_expression_1 = require("./parse-range-expression");
|
|
5
|
+
// Helper to transform FirstPassExpression into Expression
|
|
6
|
+
const transformToExpression = (expr, schemas) => {
|
|
7
|
+
switch (expr.type) {
|
|
8
|
+
case "NOT":
|
|
9
|
+
return {
|
|
10
|
+
type: "NOT",
|
|
11
|
+
expression: (0, exports.transformToExpression)(expr.expression, schemas),
|
|
12
|
+
position: expr.position,
|
|
13
|
+
length: expr.length,
|
|
14
|
+
};
|
|
15
|
+
case "WILDCARD":
|
|
16
|
+
// Check if this is part of a field:value pattern by looking at the prefix
|
|
17
|
+
const colonIndex = expr.prefix.indexOf(":");
|
|
18
|
+
if (colonIndex !== -1) {
|
|
19
|
+
const field = expr.prefix.substring(0, colonIndex).trim();
|
|
20
|
+
const prefix = expr.prefix.substring(colonIndex + 1).trim();
|
|
21
|
+
return {
|
|
22
|
+
type: "FIELD_VALUE",
|
|
23
|
+
field: {
|
|
24
|
+
type: "FIELD",
|
|
25
|
+
value: field,
|
|
26
|
+
position: expr.position - colonIndex - 1, // Adjust for the field part
|
|
27
|
+
length: colonIndex,
|
|
28
|
+
},
|
|
29
|
+
value: {
|
|
30
|
+
type: "VALUE",
|
|
31
|
+
value: prefix + "*", // Preserve the wildcard in the value
|
|
32
|
+
position: expr.position,
|
|
33
|
+
length: prefix.length + 1,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
// If not a field:value pattern, return as a wildcard search term
|
|
38
|
+
return {
|
|
39
|
+
type: "WILDCARD",
|
|
40
|
+
prefix: expr.prefix,
|
|
41
|
+
quoted: expr.quoted,
|
|
42
|
+
position: expr.position,
|
|
43
|
+
length: expr.length,
|
|
44
|
+
};
|
|
45
|
+
case "STRING": {
|
|
46
|
+
// Check if the string is a field:value pattern
|
|
47
|
+
const colonIndex = expr.value.indexOf(":");
|
|
48
|
+
if (colonIndex !== -1) {
|
|
49
|
+
const field = expr.value.substring(0, colonIndex).trim();
|
|
50
|
+
let value = expr.value.substring(colonIndex + 1).trim();
|
|
51
|
+
// Remove quotes if present
|
|
52
|
+
value =
|
|
53
|
+
value.startsWith('"') && value.endsWith('"')
|
|
54
|
+
? value.slice(1, -1)
|
|
55
|
+
: value;
|
|
56
|
+
const schema = schemas.get(field.toLowerCase());
|
|
57
|
+
// Check for range patterns when we have a numeric or date field
|
|
58
|
+
if (schema && (schema.type === "number" || schema.type === "date")) {
|
|
59
|
+
return (0, parse_range_expression_1.parseRangeExpression)(field, value, schema, expr.position, colonIndex);
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
type: "FIELD_VALUE",
|
|
63
|
+
field: {
|
|
64
|
+
type: "FIELD",
|
|
65
|
+
value: field,
|
|
66
|
+
position: expr.position,
|
|
67
|
+
length: colonIndex,
|
|
68
|
+
},
|
|
69
|
+
value: {
|
|
70
|
+
type: "VALUE",
|
|
71
|
+
value,
|
|
72
|
+
position: expr.position + colonIndex + 1,
|
|
73
|
+
length: value.length,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
type: "SEARCH_TERM",
|
|
79
|
+
value: expr.value,
|
|
80
|
+
position: expr.position,
|
|
81
|
+
length: expr.length,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
case "AND":
|
|
85
|
+
return {
|
|
86
|
+
type: "AND",
|
|
87
|
+
left: (0, exports.transformToExpression)(expr.left, schemas),
|
|
88
|
+
right: (0, exports.transformToExpression)(expr.right, schemas),
|
|
89
|
+
position: expr.position,
|
|
90
|
+
length: expr.length,
|
|
91
|
+
};
|
|
92
|
+
case "OR":
|
|
93
|
+
return {
|
|
94
|
+
type: "OR",
|
|
95
|
+
left: (0, exports.transformToExpression)(expr.left, schemas),
|
|
96
|
+
right: (0, exports.transformToExpression)(expr.right, schemas),
|
|
97
|
+
position: expr.position,
|
|
98
|
+
length: expr.length,
|
|
99
|
+
};
|
|
100
|
+
case "IN": {
|
|
101
|
+
const schema = schemas.get(expr.field.toLowerCase());
|
|
102
|
+
const transformedValues = expr.values.map((value, index) => {
|
|
103
|
+
let transformedValue = value;
|
|
104
|
+
// Handle type conversion based on schema
|
|
105
|
+
if ((schema === null || schema === void 0 ? void 0 : schema.type) === "number") {
|
|
106
|
+
transformedValue = String(Number(value));
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
type: "VALUE",
|
|
110
|
+
value: transformedValue,
|
|
111
|
+
position: expr.position + expr.field.length + 3 + index * (value.length + 1), // +3 for ":IN"
|
|
112
|
+
length: value.length,
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
return {
|
|
116
|
+
type: "IN",
|
|
117
|
+
field: {
|
|
118
|
+
type: "FIELD",
|
|
119
|
+
value: expr.field,
|
|
120
|
+
position: expr.position,
|
|
121
|
+
length: expr.field.length,
|
|
122
|
+
},
|
|
123
|
+
values: transformedValues,
|
|
124
|
+
position: expr.position,
|
|
125
|
+
length: expr.length,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
exports.transformToExpression = transformToExpression;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateExpressionFields = void 0;
|
|
4
|
+
// Helper to validate numeric values
|
|
5
|
+
const validateNumber = (value, position, errors) => {
|
|
6
|
+
if (value === "")
|
|
7
|
+
return false;
|
|
8
|
+
if (isNaN(Number(value))) {
|
|
9
|
+
errors.push({
|
|
10
|
+
message: "Invalid numeric value",
|
|
11
|
+
position,
|
|
12
|
+
length: value.length,
|
|
13
|
+
});
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
return true;
|
|
17
|
+
};
|
|
18
|
+
// Helper to validate range values for numeric fields
|
|
19
|
+
const validateNumericRange = (start, end, basePosition, errors) => {
|
|
20
|
+
let isValid = true;
|
|
21
|
+
const startPos = basePosition;
|
|
22
|
+
const endPos = basePosition + start.length + 2; // +2 for the '..'
|
|
23
|
+
if (start && !validateNumber(start, startPos, errors)) {
|
|
24
|
+
isValid = false;
|
|
25
|
+
}
|
|
26
|
+
if (end && !validateNumber(end, endPos, errors)) {
|
|
27
|
+
isValid = false;
|
|
28
|
+
}
|
|
29
|
+
if (isValid && start && end) {
|
|
30
|
+
const startNum = Number(start);
|
|
31
|
+
const endNum = Number(end);
|
|
32
|
+
if (startNum > endNum) {
|
|
33
|
+
errors.push({
|
|
34
|
+
message: "Range start must be less than or equal to range end",
|
|
35
|
+
position: basePosition,
|
|
36
|
+
length: start.length + 2 + end.length,
|
|
37
|
+
});
|
|
38
|
+
isValid = false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return isValid;
|
|
42
|
+
};
|
|
43
|
+
// Helper to validate numeric comparison operators
|
|
44
|
+
const validateNumericComparison = (operator, value, basePosition, errors) => {
|
|
45
|
+
const valuePosition = basePosition + operator.length;
|
|
46
|
+
return validateNumber(value, valuePosition, errors);
|
|
47
|
+
};
|
|
48
|
+
// Field validation helpers
|
|
49
|
+
const validateFieldValue = (expr, allowedFields, errors, schemas) => {
|
|
50
|
+
switch (expr.type) {
|
|
51
|
+
case "IN": {
|
|
52
|
+
if (!allowedFields.has(expr.field.toLowerCase())) {
|
|
53
|
+
errors.push({
|
|
54
|
+
message: `Invalid field: "${expr.field}"`,
|
|
55
|
+
position: expr.position,
|
|
56
|
+
length: expr.field.length,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
// Get schema for type validation
|
|
60
|
+
const schema = schemas.get(expr.field.toLowerCase());
|
|
61
|
+
if (schema) {
|
|
62
|
+
expr.values.forEach((value, index) => {
|
|
63
|
+
switch (schema.type) {
|
|
64
|
+
case "number":
|
|
65
|
+
if (isNaN(Number(value))) {
|
|
66
|
+
errors.push({
|
|
67
|
+
message: "Invalid numeric value",
|
|
68
|
+
position: expr.position +
|
|
69
|
+
expr.field.length +
|
|
70
|
+
4 +
|
|
71
|
+
index * (value.length + 1),
|
|
72
|
+
length: value.length,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
case "date":
|
|
77
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
78
|
+
errors.push({
|
|
79
|
+
message: "Invalid date format",
|
|
80
|
+
position: expr.position +
|
|
81
|
+
expr.field.length +
|
|
82
|
+
3 +
|
|
83
|
+
index * (value.length + 1),
|
|
84
|
+
length: value.length,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
case "WILDCARD": {
|
|
94
|
+
// For wildcard patterns, validate against field type constraints
|
|
95
|
+
const schema = schemas.get(expr.prefix.toLowerCase());
|
|
96
|
+
if ((schema === null || schema === void 0 ? void 0 : schema.type) === "number" || (schema === null || schema === void 0 ? void 0 : schema.type) === "date") {
|
|
97
|
+
errors.push({
|
|
98
|
+
message: `Wildcards are not allowed for ${schema.type} fields`,
|
|
99
|
+
position: expr.position,
|
|
100
|
+
length: expr.length,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
case "STRING": {
|
|
106
|
+
const colonIndex = expr.value.indexOf(":");
|
|
107
|
+
if (colonIndex === -1)
|
|
108
|
+
return;
|
|
109
|
+
const fieldName = expr.value.substring(0, colonIndex).trim();
|
|
110
|
+
const value = expr.value.substring(colonIndex + 1).trim();
|
|
111
|
+
if (!allowedFields.has(fieldName.toLowerCase()) && colonIndex > 0) {
|
|
112
|
+
errors.push({
|
|
113
|
+
message: `Invalid field: "${fieldName}"`,
|
|
114
|
+
position: expr.position,
|
|
115
|
+
length: colonIndex,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (!value) {
|
|
119
|
+
errors.push({
|
|
120
|
+
message: "Expected field value",
|
|
121
|
+
position: expr.position,
|
|
122
|
+
length: colonIndex + 1,
|
|
123
|
+
});
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (value.startsWith(":")) {
|
|
127
|
+
errors.push({
|
|
128
|
+
message: "Missing field name",
|
|
129
|
+
position: expr.position,
|
|
130
|
+
length: value.length + colonIndex + 1,
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const schema = schemas.get(fieldName.toLowerCase());
|
|
135
|
+
if (!schema)
|
|
136
|
+
return;
|
|
137
|
+
const valueStartPosition = expr.position + colonIndex + 1;
|
|
138
|
+
if (schema.type === "number") {
|
|
139
|
+
if (value.includes("..")) {
|
|
140
|
+
if (value === ".." || value.includes("...")) {
|
|
141
|
+
errors.push({
|
|
142
|
+
message: "Invalid range format",
|
|
143
|
+
position: valueStartPosition,
|
|
144
|
+
length: value.length,
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const [start, end] = value.split("..");
|
|
149
|
+
validateNumericRange(start, end, valueStartPosition, errors);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const comparisonMatch = value.match(/^(>=|>|<=|<)(.*)$/);
|
|
153
|
+
if (comparisonMatch) {
|
|
154
|
+
const [, operator, compValue] = comparisonMatch;
|
|
155
|
+
const invalidOp = /^[<>]{2,}|>=>/;
|
|
156
|
+
if (invalidOp.test(value)) {
|
|
157
|
+
errors.push({
|
|
158
|
+
message: "Invalid range operator",
|
|
159
|
+
position: valueStartPosition,
|
|
160
|
+
length: 3,
|
|
161
|
+
});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (!compValue) {
|
|
165
|
+
errors.push({
|
|
166
|
+
message: "Expected range value",
|
|
167
|
+
position: valueStartPosition + operator.length,
|
|
168
|
+
length: 0,
|
|
169
|
+
});
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
validateNumericComparison(operator, compValue, valueStartPosition, errors);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
validateNumber(value, valueStartPosition, errors);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (schema.type === "date") {
|
|
179
|
+
const dateValidator = (dateStr) => {
|
|
180
|
+
if (!dateStr)
|
|
181
|
+
return true;
|
|
182
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
const date = new Date(dateStr);
|
|
186
|
+
return (!isNaN(date.getTime()) &&
|
|
187
|
+
dateStr === date.toISOString().split("T")[0]);
|
|
188
|
+
};
|
|
189
|
+
if (value.includes("..")) {
|
|
190
|
+
const [start, end] = value.split("..");
|
|
191
|
+
if (!dateValidator(start) || !dateValidator(end)) {
|
|
192
|
+
errors.push({
|
|
193
|
+
message: "Invalid date format",
|
|
194
|
+
position: valueStartPosition,
|
|
195
|
+
length: value.length,
|
|
196
|
+
});
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
const comparisonMatch = value.match(/^(>=|>|<=|<)(.*)$/);
|
|
202
|
+
if (comparisonMatch) {
|
|
203
|
+
const [, , dateStr] = comparisonMatch;
|
|
204
|
+
if (!dateValidator(dateStr)) {
|
|
205
|
+
errors.push({
|
|
206
|
+
message: "Invalid date format",
|
|
207
|
+
position: valueStartPosition,
|
|
208
|
+
length: value.length,
|
|
209
|
+
});
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else if (!dateValidator(value)) {
|
|
214
|
+
errors.push({
|
|
215
|
+
message: "Invalid date format",
|
|
216
|
+
position: valueStartPosition,
|
|
217
|
+
length: value.length,
|
|
218
|
+
});
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
const validateExpressionFields = (expr, allowedFields, errors, schemas) => {
|
|
228
|
+
switch (expr.type) {
|
|
229
|
+
case "STRING":
|
|
230
|
+
case "WILDCARD":
|
|
231
|
+
case "IN":
|
|
232
|
+
validateFieldValue(expr, allowedFields, errors, schemas);
|
|
233
|
+
break;
|
|
234
|
+
case "AND":
|
|
235
|
+
case "OR":
|
|
236
|
+
(0, exports.validateExpressionFields)(expr.left, allowedFields, errors, schemas);
|
|
237
|
+
(0, exports.validateExpressionFields)(expr.right, allowedFields, errors, schemas);
|
|
238
|
+
break;
|
|
239
|
+
case "NOT":
|
|
240
|
+
(0, exports.validateExpressionFields)(expr.expression, allowedFields, errors, schemas);
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
exports.validateExpressionFields = validateExpressionFields;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateInExpression = void 0;
|
|
4
|
+
const validator_1 = require("./validator");
|
|
5
|
+
const validateInExpression = (expr, errors) => {
|
|
6
|
+
// Validate field name pattern
|
|
7
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(expr.field)) {
|
|
8
|
+
errors.push({
|
|
9
|
+
message: "Invalid characters in field name",
|
|
10
|
+
position: expr.position,
|
|
11
|
+
length: expr.field.length,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
// Check for reserved words
|
|
15
|
+
if (validator_1.reservedWords.has(expr.field.toUpperCase())) {
|
|
16
|
+
errors.push({
|
|
17
|
+
message: `${expr.field} is a reserved word`,
|
|
18
|
+
position: expr.position,
|
|
19
|
+
length: expr.field.length,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
// Validate value format based on field type
|
|
23
|
+
expr.values.forEach((value, index) => {
|
|
24
|
+
if (value.includes(",")) {
|
|
25
|
+
errors.push({
|
|
26
|
+
message: "Invalid character in IN value",
|
|
27
|
+
position: expr.position + expr.field.length + 3 + index * (value.length + 1),
|
|
28
|
+
length: value.length,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
exports.validateInExpression = validateInExpression;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateString = void 0;
|
|
4
|
+
const validator_1 = require("./validator");
|
|
5
|
+
const validate_wildcard_1 = require("./validate-wildcard");
|
|
6
|
+
// Validate individual strings (field:value pairs or plain terms)
|
|
7
|
+
const validateString = (expr, errors) => {
|
|
8
|
+
// Validate wildcard usage
|
|
9
|
+
(0, validate_wildcard_1.validateWildcard)(expr, errors);
|
|
10
|
+
// For wildcard patterns, no additional validation needed
|
|
11
|
+
if (expr.type === "WILDCARD") {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
// Handle STRING type
|
|
15
|
+
// Check for empty field values
|
|
16
|
+
if (expr.value.endsWith(":")) {
|
|
17
|
+
errors.push({
|
|
18
|
+
message: "Expected field value",
|
|
19
|
+
position: expr.position,
|
|
20
|
+
length: expr.length,
|
|
21
|
+
});
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
// Check for field values that start with colon
|
|
25
|
+
if (expr.value.startsWith(":")) {
|
|
26
|
+
errors.push({
|
|
27
|
+
message: "Missing field name",
|
|
28
|
+
position: expr.position,
|
|
29
|
+
length: expr.length,
|
|
30
|
+
});
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// For field:value patterns, validate the field name
|
|
34
|
+
if (expr.value.includes(":")) {
|
|
35
|
+
const [fieldName] = expr.value.split(":");
|
|
36
|
+
// Check for reserved words used as field names
|
|
37
|
+
if (validator_1.reservedWords.has(fieldName.toUpperCase())) {
|
|
38
|
+
errors.push({
|
|
39
|
+
message: `${fieldName} is a reserved word`,
|
|
40
|
+
position: expr.position,
|
|
41
|
+
length: fieldName.length,
|
|
42
|
+
});
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Check for invalid characters in field names
|
|
46
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(fieldName)) {
|
|
47
|
+
errors.push({
|
|
48
|
+
message: "Invalid characters in field name",
|
|
49
|
+
position: expr.position,
|
|
50
|
+
length: fieldName.length,
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Handle standalone reserved words (not in field:value pattern)
|
|
56
|
+
if (!expr.value.includes(":") &&
|
|
57
|
+
validator_1.reservedWords.has(expr.value.toUpperCase())) {
|
|
58
|
+
errors.push({
|
|
59
|
+
message: `${expr.value} is a reserved word`,
|
|
60
|
+
position: expr.position,
|
|
61
|
+
length: expr.length,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
exports.validateString = validateString;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateWildcard = void 0;
|
|
4
|
+
// Validates wildcard patterns
|
|
5
|
+
const validateWildcard = (expr, errors) => {
|
|
6
|
+
const value = expr.type === "STRING" ? expr.value : expr.prefix + "*";
|
|
7
|
+
const starCount = (value.match(/\*/g) || []).length;
|
|
8
|
+
const isQuoted = expr.quoted;
|
|
9
|
+
// For unquoted strings
|
|
10
|
+
if (!isQuoted) {
|
|
11
|
+
const firstStar = value.indexOf("*");
|
|
12
|
+
if (starCount > 1) {
|
|
13
|
+
const secondStar = value.indexOf("*", firstStar + 1);
|
|
14
|
+
errors.push({
|
|
15
|
+
message: "Only one trailing wildcard (*) is allowed",
|
|
16
|
+
position: expr.position + secondStar,
|
|
17
|
+
length: 1,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
if ((firstStar !== -1 && firstStar !== value.length - 1) && !value.endsWith("**")) {
|
|
21
|
+
errors.push({
|
|
22
|
+
message: "Wildcard (*) can only appear at the end of a term",
|
|
23
|
+
position: expr.position + firstStar,
|
|
24
|
+
length: 1,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// For quoted strings
|
|
29
|
+
else {
|
|
30
|
+
// Handle multiple wildcards or internal wildcards in quoted strings
|
|
31
|
+
if (value.endsWith("**")) {
|
|
32
|
+
errors.push({
|
|
33
|
+
message: "Only one trailing wildcard (*) is allowed",
|
|
34
|
+
position: expr.position + value.length - 1,
|
|
35
|
+
length: 1,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
exports.validateWildcard = validateWildcard;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateSearchQuery = exports.reservedWords = void 0;
|
|
4
|
+
const validate_in_expression_1 = require("./validate-in-expression");
|
|
5
|
+
const validate_string_1 = require("./validate-string");
|
|
6
|
+
exports.reservedWords = new Set(["AND", "OR"]);
|
|
7
|
+
const walkExpression = (expr, errors) => {
|
|
8
|
+
switch (expr.type) {
|
|
9
|
+
case "STRING":
|
|
10
|
+
case "WILDCARD":
|
|
11
|
+
(0, validate_string_1.validateString)(expr, errors);
|
|
12
|
+
break;
|
|
13
|
+
case "AND":
|
|
14
|
+
case "OR":
|
|
15
|
+
walkExpression(expr.left, errors);
|
|
16
|
+
walkExpression(expr.right, errors);
|
|
17
|
+
break;
|
|
18
|
+
case "NOT":
|
|
19
|
+
walkExpression(expr.expression, errors);
|
|
20
|
+
break;
|
|
21
|
+
case "IN":
|
|
22
|
+
(0, validate_in_expression_1.validateInExpression)(expr, errors);
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const validateSearchQuery = (expression) => {
|
|
27
|
+
const errors = [];
|
|
28
|
+
if (expression === null) {
|
|
29
|
+
return errors;
|
|
30
|
+
}
|
|
31
|
+
walkExpression(expression, errors);
|
|
32
|
+
return errors;
|
|
33
|
+
};
|
|
34
|
+
exports.validateSearchQuery = validateSearchQuery;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { TokenType, currentToken, advanceStream } from "./lexer";
|
|
2
|
+
import { parsePrimary } from "./parse-primary";
|
|
3
|
+
const getOperatorPrecedence = (type) => type === TokenType.AND ? 2 : type === TokenType.OR ? 1 : 0;
|
|
4
|
+
export const parseExpression = (stream, minPrecedence = 0) => {
|
|
5
|
+
const token = currentToken(stream);
|
|
6
|
+
if (token.type === TokenType.STRING && token.value === "*") {
|
|
7
|
+
return {
|
|
8
|
+
result: {
|
|
9
|
+
type: "WILDCARD",
|
|
10
|
+
prefix: "",
|
|
11
|
+
quoted: false,
|
|
12
|
+
position: token.position,
|
|
13
|
+
length: token.length,
|
|
14
|
+
},
|
|
15
|
+
stream: advanceStream(stream),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
let result = parsePrimary(stream);
|
|
19
|
+
while (true) {
|
|
20
|
+
const token = currentToken(result.stream);
|
|
21
|
+
if (token.type === TokenType.EOF)
|
|
22
|
+
break;
|
|
23
|
+
if (token.type === TokenType.AND || token.type === TokenType.OR) {
|
|
24
|
+
const precedence = getOperatorPrecedence(token.type);
|
|
25
|
+
if (precedence < minPrecedence)
|
|
26
|
+
break;
|
|
27
|
+
const operator = token.type;
|
|
28
|
+
const nextStream = advanceStream(result.stream);
|
|
29
|
+
const nextToken = currentToken(nextStream);
|
|
30
|
+
if (nextToken.type === TokenType.EOF) {
|
|
31
|
+
throw {
|
|
32
|
+
message: `Unexpected token: ${token.value}`,
|
|
33
|
+
position: token.position,
|
|
34
|
+
length: token.length,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const right = parseExpression(nextStream, precedence + 1);
|
|
38
|
+
result = {
|
|
39
|
+
result: {
|
|
40
|
+
type: operator,
|
|
41
|
+
left: result.result,
|
|
42
|
+
right: right.result,
|
|
43
|
+
position: token.position,
|
|
44
|
+
length: token.length,
|
|
45
|
+
},
|
|
46
|
+
stream: right.stream,
|
|
47
|
+
};
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (token.type === TokenType.STRING ||
|
|
51
|
+
token.type === TokenType.QUOTED_STRING ||
|
|
52
|
+
token.type === TokenType.LPAREN ||
|
|
53
|
+
token.type === TokenType.NOT) {
|
|
54
|
+
const precedence = getOperatorPrecedence(TokenType.AND);
|
|
55
|
+
if (precedence < minPrecedence)
|
|
56
|
+
break;
|
|
57
|
+
const right = parseExpression(result.stream, precedence + 1);
|
|
58
|
+
result = {
|
|
59
|
+
result: {
|
|
60
|
+
type: TokenType.AND,
|
|
61
|
+
left: result.result,
|
|
62
|
+
right: right.result,
|
|
63
|
+
position: token.position,
|
|
64
|
+
length: token.length,
|
|
65
|
+
},
|
|
66
|
+
stream: right.stream,
|
|
67
|
+
};
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
};
|