search-input-query-parser 0.1.3 → 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.
- package/dist/cjs/first-pass-parser.js +3 -0
- package/dist/cjs/lexer.js +9 -1
- package/dist/cjs/parse-in-values.js +5 -0
- package/dist/cjs/parse-primary.js +7 -0
- package/dist/cjs/parser.js +3 -1
- package/dist/cjs/validate-expression-fields.js +19 -0
- package/dist/cjs/validate-in-expression.js +4 -0
- package/dist/cjs/validate-string.js +7 -0
- package/dist/cjs/validate-wildcard.js +4 -0
- package/dist/cjs/validator.js +35 -1
- package/dist/esm/first-pass-parser.js +3 -0
- package/dist/esm/lexer.js +9 -1
- package/dist/esm/parse-in-values.js +5 -0
- package/dist/esm/parse-primary.js +7 -0
- package/dist/esm/parser.js +3 -2
- package/dist/esm/validate-expression-fields.js +19 -0
- package/dist/esm/validate-in-expression.js +5 -1
- package/dist/esm/validate-string.js +8 -1
- package/dist/esm/validate-wildcard.js +4 -0
- package/dist/esm/validator.js +34 -0
- package/dist/types/parser.d.ts +2 -2
- package/dist/types/validator.d.ts +30 -0
- package/package.json +5 -4
- package/src/first-pass-parser.ts +3 -0
- package/src/lexer.ts +10 -1
- package/src/parse-in-values.ts +5 -1
- package/src/parse-primary.ts +7 -0
- package/src/parser.test.ts +72 -0
- package/src/parser.ts +7 -1
- package/src/validate-expression-fields.ts +19 -1
- package/src/validate-in-expression.ts +5 -1
- package/src/validate-string.ts +12 -1
- package/src/validate-wildcard.ts +4 -1
- package/src/validator.test.ts +25 -1
- package/src/validator.ts +40 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SearchQueryErrorCode } from "./validator";
|
|
1
2
|
// Helper to validate numeric values
|
|
2
3
|
const validateNumber = (value, position, errors) => {
|
|
3
4
|
if (value === "")
|
|
@@ -5,6 +6,7 @@ const validateNumber = (value, position, errors) => {
|
|
|
5
6
|
if (isNaN(Number(value))) {
|
|
6
7
|
errors.push({
|
|
7
8
|
message: "Invalid numeric value",
|
|
9
|
+
code: SearchQueryErrorCode.INVALID_NUMERIC_VALUE,
|
|
8
10
|
position,
|
|
9
11
|
length: value.length,
|
|
10
12
|
});
|
|
@@ -29,6 +31,7 @@ const validateNumericRange = (start, end, basePosition, errors) => {
|
|
|
29
31
|
if (startNum > endNum) {
|
|
30
32
|
errors.push({
|
|
31
33
|
message: "Range start must be less than or equal to range end",
|
|
34
|
+
code: SearchQueryErrorCode.RANGE_START_GREATER_THAN_END,
|
|
32
35
|
position: basePosition,
|
|
33
36
|
length: start.length + 2 + end.length,
|
|
34
37
|
});
|
|
@@ -49,6 +52,8 @@ const validateFieldValue = (expr, allowedFields, errors, schemas) => {
|
|
|
49
52
|
if (!allowedFields.has(expr.field.toLowerCase())) {
|
|
50
53
|
errors.push({
|
|
51
54
|
message: `Invalid field: "${expr.field}"`,
|
|
55
|
+
code: SearchQueryErrorCode.INVALID_FIELD_NAME,
|
|
56
|
+
value: expr.field,
|
|
52
57
|
position: expr.position,
|
|
53
58
|
length: expr.field.length,
|
|
54
59
|
});
|
|
@@ -62,6 +67,7 @@ const validateFieldValue = (expr, allowedFields, errors, schemas) => {
|
|
|
62
67
|
if (isNaN(Number(value))) {
|
|
63
68
|
errors.push({
|
|
64
69
|
message: "Invalid numeric value",
|
|
70
|
+
code: SearchQueryErrorCode.INVALID_NUMERIC_VALUE,
|
|
65
71
|
position: expr.position +
|
|
66
72
|
expr.field.length +
|
|
67
73
|
4 +
|
|
@@ -74,6 +80,7 @@ const validateFieldValue = (expr, allowedFields, errors, schemas) => {
|
|
|
74
80
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
75
81
|
errors.push({
|
|
76
82
|
message: "Invalid date format",
|
|
83
|
+
code: SearchQueryErrorCode.INVALID_DATE_FORMAT,
|
|
77
84
|
position: expr.position +
|
|
78
85
|
expr.field.length +
|
|
79
86
|
3 +
|
|
@@ -92,6 +99,8 @@ const validateFieldValue = (expr, allowedFields, errors, schemas) => {
|
|
|
92
99
|
const schema = schemas.get(expr.prefix.toLowerCase());
|
|
93
100
|
if ((schema === null || schema === void 0 ? void 0 : schema.type) === "number" || (schema === null || schema === void 0 ? void 0 : schema.type) === "date") {
|
|
94
101
|
errors.push({
|
|
102
|
+
code: SearchQueryErrorCode.WILDCARD_NOT_ALLOWED,
|
|
103
|
+
value: schema.type,
|
|
95
104
|
message: `Wildcards are not allowed for ${schema.type} fields`,
|
|
96
105
|
position: expr.position,
|
|
97
106
|
length: expr.length,
|
|
@@ -108,6 +117,8 @@ const validateFieldValue = (expr, allowedFields, errors, schemas) => {
|
|
|
108
117
|
if (!allowedFields.has(fieldName.toLowerCase()) && colonIndex > 0) {
|
|
109
118
|
errors.push({
|
|
110
119
|
message: `Invalid field: "${fieldName}"`,
|
|
120
|
+
code: SearchQueryErrorCode.INVALID_FIELD_NAME,
|
|
121
|
+
value: fieldName,
|
|
111
122
|
position: expr.position,
|
|
112
123
|
length: colonIndex,
|
|
113
124
|
});
|
|
@@ -115,6 +126,7 @@ const validateFieldValue = (expr, allowedFields, errors, schemas) => {
|
|
|
115
126
|
if (!value) {
|
|
116
127
|
errors.push({
|
|
117
128
|
message: "Expected field value",
|
|
129
|
+
code: SearchQueryErrorCode.EXPECTED_FIELD_VALUE,
|
|
118
130
|
position: expr.position,
|
|
119
131
|
length: colonIndex + 1,
|
|
120
132
|
});
|
|
@@ -123,6 +135,7 @@ const validateFieldValue = (expr, allowedFields, errors, schemas) => {
|
|
|
123
135
|
if (value.startsWith(":")) {
|
|
124
136
|
errors.push({
|
|
125
137
|
message: "Missing field name",
|
|
138
|
+
code: SearchQueryErrorCode.MISSING_FIELD_NAME,
|
|
126
139
|
position: expr.position,
|
|
127
140
|
length: value.length + colonIndex + 1,
|
|
128
141
|
});
|
|
@@ -137,6 +150,7 @@ const validateFieldValue = (expr, allowedFields, errors, schemas) => {
|
|
|
137
150
|
if (value === ".." || value.includes("...")) {
|
|
138
151
|
errors.push({
|
|
139
152
|
message: "Invalid range format",
|
|
153
|
+
code: SearchQueryErrorCode.INVALID_RANGE_FORMAT,
|
|
140
154
|
position: valueStartPosition,
|
|
141
155
|
length: value.length,
|
|
142
156
|
});
|
|
@@ -153,6 +167,7 @@ const validateFieldValue = (expr, allowedFields, errors, schemas) => {
|
|
|
153
167
|
if (invalidOp.test(value)) {
|
|
154
168
|
errors.push({
|
|
155
169
|
message: "Invalid range operator",
|
|
170
|
+
code: SearchQueryErrorCode.INVALID_RANGE_OPERATOR,
|
|
156
171
|
position: valueStartPosition,
|
|
157
172
|
length: 3,
|
|
158
173
|
});
|
|
@@ -161,6 +176,7 @@ const validateFieldValue = (expr, allowedFields, errors, schemas) => {
|
|
|
161
176
|
if (!compValue) {
|
|
162
177
|
errors.push({
|
|
163
178
|
message: "Expected range value",
|
|
179
|
+
code: SearchQueryErrorCode.EXPECTED_RANGE_VALUE,
|
|
164
180
|
position: valueStartPosition + operator.length,
|
|
165
181
|
length: 0,
|
|
166
182
|
});
|
|
@@ -188,6 +204,7 @@ const validateFieldValue = (expr, allowedFields, errors, schemas) => {
|
|
|
188
204
|
if (!dateValidator(start) || !dateValidator(end)) {
|
|
189
205
|
errors.push({
|
|
190
206
|
message: "Invalid date format",
|
|
207
|
+
code: SearchQueryErrorCode.INVALID_DATE_FORMAT,
|
|
191
208
|
position: valueStartPosition,
|
|
192
209
|
length: value.length,
|
|
193
210
|
});
|
|
@@ -201,6 +218,7 @@ const validateFieldValue = (expr, allowedFields, errors, schemas) => {
|
|
|
201
218
|
if (!dateValidator(dateStr)) {
|
|
202
219
|
errors.push({
|
|
203
220
|
message: "Invalid date format",
|
|
221
|
+
code: SearchQueryErrorCode.INVALID_DATE_FORMAT,
|
|
204
222
|
position: valueStartPosition,
|
|
205
223
|
length: value.length,
|
|
206
224
|
});
|
|
@@ -210,6 +228,7 @@ const validateFieldValue = (expr, allowedFields, errors, schemas) => {
|
|
|
210
228
|
else if (!dateValidator(value)) {
|
|
211
229
|
errors.push({
|
|
212
230
|
message: "Invalid date format",
|
|
231
|
+
code: SearchQueryErrorCode.INVALID_DATE_FORMAT,
|
|
213
232
|
position: valueStartPosition,
|
|
214
233
|
length: value.length,
|
|
215
234
|
});
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { reservedWords } from "./validator";
|
|
1
|
+
import { reservedWords, SearchQueryErrorCode } from "./validator";
|
|
2
2
|
export const validateInExpression = (expr, errors) => {
|
|
3
3
|
// Validate field name pattern
|
|
4
4
|
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(expr.field)) {
|
|
5
5
|
errors.push({
|
|
6
6
|
message: "Invalid characters in field name",
|
|
7
|
+
code: SearchQueryErrorCode.INVALID_FIELD_CHARS,
|
|
7
8
|
position: expr.position,
|
|
8
9
|
length: expr.field.length,
|
|
9
10
|
});
|
|
@@ -12,6 +13,8 @@ export const validateInExpression = (expr, errors) => {
|
|
|
12
13
|
if (reservedWords.has(expr.field.toUpperCase())) {
|
|
13
14
|
errors.push({
|
|
14
15
|
message: `${expr.field} is a reserved word`,
|
|
16
|
+
code: SearchQueryErrorCode.RESERVED_WORD_AS_FIELD,
|
|
17
|
+
value: expr.field,
|
|
15
18
|
position: expr.position,
|
|
16
19
|
length: expr.field.length,
|
|
17
20
|
});
|
|
@@ -21,6 +24,7 @@ export const validateInExpression = (expr, errors) => {
|
|
|
21
24
|
if (value.includes(",")) {
|
|
22
25
|
errors.push({
|
|
23
26
|
message: "Invalid character in IN value",
|
|
27
|
+
code: SearchQueryErrorCode.INVALID_IN_VALUE,
|
|
24
28
|
position: expr.position + expr.field.length + 3 + index * (value.length + 1),
|
|
25
29
|
length: value.length,
|
|
26
30
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { reservedWords } from "./validator";
|
|
1
|
+
import { reservedWords, SearchQueryErrorCode, } from "./validator";
|
|
2
2
|
import { validateWildcard } from "./validate-wildcard";
|
|
3
3
|
// Validate individual strings (field:value pairs or plain terms)
|
|
4
4
|
export const validateString = (expr, errors) => {
|
|
@@ -13,6 +13,7 @@ export const validateString = (expr, errors) => {
|
|
|
13
13
|
if (expr.value.endsWith(":")) {
|
|
14
14
|
errors.push({
|
|
15
15
|
message: "Expected field value",
|
|
16
|
+
code: SearchQueryErrorCode.EXPECTED_FIELD_VALUE,
|
|
16
17
|
position: expr.position,
|
|
17
18
|
length: expr.length,
|
|
18
19
|
});
|
|
@@ -22,6 +23,7 @@ export const validateString = (expr, errors) => {
|
|
|
22
23
|
if (expr.value.startsWith(":")) {
|
|
23
24
|
errors.push({
|
|
24
25
|
message: "Missing field name",
|
|
26
|
+
code: SearchQueryErrorCode.MISSING_FIELD_NAME,
|
|
25
27
|
position: expr.position,
|
|
26
28
|
length: expr.length,
|
|
27
29
|
});
|
|
@@ -34,6 +36,8 @@ export const validateString = (expr, errors) => {
|
|
|
34
36
|
if (reservedWords.has(fieldName.toUpperCase())) {
|
|
35
37
|
errors.push({
|
|
36
38
|
message: `${fieldName} is a reserved word`,
|
|
39
|
+
code: SearchQueryErrorCode.RESERVED_WORD_AS_FIELD,
|
|
40
|
+
value: fieldName,
|
|
37
41
|
position: expr.position,
|
|
38
42
|
length: fieldName.length,
|
|
39
43
|
});
|
|
@@ -43,6 +47,7 @@ export const validateString = (expr, errors) => {
|
|
|
43
47
|
if (!/^[a-zA-Z0-9_-]+$/.test(fieldName)) {
|
|
44
48
|
errors.push({
|
|
45
49
|
message: "Invalid characters in field name",
|
|
50
|
+
code: SearchQueryErrorCode.INVALID_FIELD_CHARS,
|
|
46
51
|
position: expr.position,
|
|
47
52
|
length: fieldName.length,
|
|
48
53
|
});
|
|
@@ -54,6 +59,8 @@ export const validateString = (expr, errors) => {
|
|
|
54
59
|
reservedWords.has(expr.value.toUpperCase())) {
|
|
55
60
|
errors.push({
|
|
56
61
|
message: `${expr.value} is a reserved word`,
|
|
62
|
+
code: SearchQueryErrorCode.RESERVED_WORD_AS_FIELD,
|
|
63
|
+
value: expr.value,
|
|
57
64
|
position: expr.position,
|
|
58
65
|
length: expr.length,
|
|
59
66
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SearchQueryErrorCode } from "./validator";
|
|
1
2
|
// Validates wildcard patterns
|
|
2
3
|
export const validateWildcard = (expr, errors) => {
|
|
3
4
|
const value = expr.type === "STRING" ? expr.value : expr.prefix + "*";
|
|
@@ -10,6 +11,7 @@ export const validateWildcard = (expr, errors) => {
|
|
|
10
11
|
const secondStar = value.indexOf("*", firstStar + 1);
|
|
11
12
|
errors.push({
|
|
12
13
|
message: "Only one trailing wildcard (*) is allowed",
|
|
14
|
+
code: SearchQueryErrorCode.MULTIPLE_WILDCARDS,
|
|
13
15
|
position: expr.position + secondStar,
|
|
14
16
|
length: 1,
|
|
15
17
|
});
|
|
@@ -17,6 +19,7 @@ export const validateWildcard = (expr, errors) => {
|
|
|
17
19
|
if ((firstStar !== -1 && firstStar !== value.length - 1) && !value.endsWith("**")) {
|
|
18
20
|
errors.push({
|
|
19
21
|
message: "Wildcard (*) can only appear at the end of a term",
|
|
22
|
+
code: SearchQueryErrorCode.INVALID_WILDCARD_POSITION,
|
|
20
23
|
position: expr.position + firstStar,
|
|
21
24
|
length: 1,
|
|
22
25
|
});
|
|
@@ -28,6 +31,7 @@ export const validateWildcard = (expr, errors) => {
|
|
|
28
31
|
if (value.endsWith("**")) {
|
|
29
32
|
errors.push({
|
|
30
33
|
message: "Only one trailing wildcard (*) is allowed",
|
|
34
|
+
code: SearchQueryErrorCode.MULTIPLE_WILDCARDS,
|
|
31
35
|
position: expr.position + value.length - 1,
|
|
32
36
|
length: 1,
|
|
33
37
|
});
|
package/dist/esm/validator.js
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
import { validateInExpression } from "./validate-in-expression";
|
|
2
2
|
import { validateString } from "./validate-string";
|
|
3
|
+
export var SearchQueryErrorCode;
|
|
4
|
+
(function (SearchQueryErrorCode) {
|
|
5
|
+
// Syntax Errors (1000-1999)
|
|
6
|
+
SearchQueryErrorCode[SearchQueryErrorCode["UNTERMINATED_QUOTED_STRING"] = 1001] = "UNTERMINATED_QUOTED_STRING";
|
|
7
|
+
SearchQueryErrorCode[SearchQueryErrorCode["EXPECTED_FIELD_VALUE"] = 1002] = "EXPECTED_FIELD_VALUE";
|
|
8
|
+
SearchQueryErrorCode[SearchQueryErrorCode["MISSING_FIELD_NAME"] = 1003] = "MISSING_FIELD_NAME";
|
|
9
|
+
SearchQueryErrorCode[SearchQueryErrorCode["MISSING_FIELD_VALUE"] = 1004] = "MISSING_FIELD_VALUE";
|
|
10
|
+
SearchQueryErrorCode[SearchQueryErrorCode["UNEXPECTED_RIGHT_PAREN"] = 1005] = "UNEXPECTED_RIGHT_PAREN";
|
|
11
|
+
SearchQueryErrorCode[SearchQueryErrorCode["EXPECTED_RIGHT_PAREN"] = 1006] = "EXPECTED_RIGHT_PAREN";
|
|
12
|
+
SearchQueryErrorCode[SearchQueryErrorCode["UNEXPECTED_TOKEN"] = 1007] = "UNEXPECTED_TOKEN";
|
|
13
|
+
SearchQueryErrorCode[SearchQueryErrorCode["EXPECTED_TOKEN"] = 1008] = "EXPECTED_TOKEN";
|
|
14
|
+
SearchQueryErrorCode[SearchQueryErrorCode["MISSING_OPERATOR_OR_WHITESPACE"] = 1009] = "MISSING_OPERATOR_OR_WHITESPACE";
|
|
15
|
+
SearchQueryErrorCode[SearchQueryErrorCode["RESERVED_WORD"] = 1010] = "RESERVED_WORD";
|
|
16
|
+
// Field Validation Errors (2000-2999)
|
|
17
|
+
SearchQueryErrorCode[SearchQueryErrorCode["INVALID_FIELD_NAME"] = 2001] = "INVALID_FIELD_NAME";
|
|
18
|
+
SearchQueryErrorCode[SearchQueryErrorCode["INVALID_FIELD_CHARS"] = 2002] = "INVALID_FIELD_CHARS";
|
|
19
|
+
SearchQueryErrorCode[SearchQueryErrorCode["RESERVED_WORD_AS_FIELD"] = 2003] = "RESERVED_WORD_AS_FIELD";
|
|
20
|
+
// Value Validation Errors (3000-3999)
|
|
21
|
+
SearchQueryErrorCode[SearchQueryErrorCode["INVALID_NUMERIC_VALUE"] = 3001] = "INVALID_NUMERIC_VALUE";
|
|
22
|
+
SearchQueryErrorCode[SearchQueryErrorCode["INVALID_DATE_FORMAT"] = 3002] = "INVALID_DATE_FORMAT";
|
|
23
|
+
SearchQueryErrorCode[SearchQueryErrorCode["INVALID_RANGE_FORMAT"] = 3003] = "INVALID_RANGE_FORMAT";
|
|
24
|
+
SearchQueryErrorCode[SearchQueryErrorCode["INVALID_RANGE_OPERATOR"] = 3004] = "INVALID_RANGE_OPERATOR";
|
|
25
|
+
SearchQueryErrorCode[SearchQueryErrorCode["EXPECTED_RANGE_VALUE"] = 3005] = "EXPECTED_RANGE_VALUE";
|
|
26
|
+
SearchQueryErrorCode[SearchQueryErrorCode["RANGE_START_GREATER_THAN_END"] = 3006] = "RANGE_START_GREATER_THAN_END";
|
|
27
|
+
SearchQueryErrorCode[SearchQueryErrorCode["WILDCARD_NOT_ALLOWED"] = 3007] = "WILDCARD_NOT_ALLOWED";
|
|
28
|
+
// Wildcard Errors (4000-4999)
|
|
29
|
+
SearchQueryErrorCode[SearchQueryErrorCode["INVALID_WILDCARD_POSITION"] = 4001] = "INVALID_WILDCARD_POSITION";
|
|
30
|
+
SearchQueryErrorCode[SearchQueryErrorCode["MULTIPLE_WILDCARDS"] = 4002] = "MULTIPLE_WILDCARDS";
|
|
31
|
+
// IN Expression Errors (5000-5999)
|
|
32
|
+
SearchQueryErrorCode[SearchQueryErrorCode["EMPTY_IN_LIST"] = 5001] = "EMPTY_IN_LIST";
|
|
33
|
+
SearchQueryErrorCode[SearchQueryErrorCode["INVALID_IN_VALUE"] = 5002] = "INVALID_IN_VALUE";
|
|
34
|
+
SearchQueryErrorCode[SearchQueryErrorCode["EXPECTED_IN_SEPARATOR"] = 5003] = "EXPECTED_IN_SEPARATOR";
|
|
35
|
+
SearchQueryErrorCode[SearchQueryErrorCode["EXPECTED_LPAREN_AFTER_IN"] = 5004] = "EXPECTED_LPAREN_AFTER_IN";
|
|
36
|
+
})(SearchQueryErrorCode || (SearchQueryErrorCode = {}));
|
|
3
37
|
export const reservedWords = new Set(["AND", "OR"]);
|
|
4
38
|
const walkExpression = (expr, errors) => {
|
|
5
39
|
switch (expr.type) {
|
package/dist/types/parser.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PositionLength } from "./first-pass-parser";
|
|
2
|
-
import { ValidationError } from "./validator";
|
|
2
|
+
import { ValidationError, SearchQueryErrorCode } from "./validator";
|
|
3
3
|
interface FieldSchema {
|
|
4
4
|
name: string;
|
|
5
5
|
type: "string" | "number" | "date" | "boolean";
|
|
@@ -65,4 +65,4 @@ type SearchQueryError = {
|
|
|
65
65
|
};
|
|
66
66
|
declare const stringify: (expr: Expression) => string;
|
|
67
67
|
export declare const parseSearchInputQuery: (input: string, fieldSchemas?: FieldSchema[]) => SearchQuery | SearchQueryError;
|
|
68
|
-
export { type SearchQuery, type SearchQueryError, type Expression, type ValidationError, type FieldSchema, type RangeOperator, type RangeExpression, type WildcardPattern, type Value, stringify, };
|
|
68
|
+
export { type SearchQuery, type SearchQueryError, type Expression, type ValidationError, SearchQueryErrorCode, type FieldSchema, type RangeOperator, type RangeExpression, type WildcardPattern, type Value, stringify, };
|
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
import { FirstPassExpression } from "./first-pass-parser";
|
|
2
|
+
export declare enum SearchQueryErrorCode {
|
|
3
|
+
UNTERMINATED_QUOTED_STRING = 1001,
|
|
4
|
+
EXPECTED_FIELD_VALUE = 1002,
|
|
5
|
+
MISSING_FIELD_NAME = 1003,
|
|
6
|
+
MISSING_FIELD_VALUE = 1004,
|
|
7
|
+
UNEXPECTED_RIGHT_PAREN = 1005,
|
|
8
|
+
EXPECTED_RIGHT_PAREN = 1006,
|
|
9
|
+
UNEXPECTED_TOKEN = 1007,
|
|
10
|
+
EXPECTED_TOKEN = 1008,
|
|
11
|
+
MISSING_OPERATOR_OR_WHITESPACE = 1009,
|
|
12
|
+
RESERVED_WORD = 1010,
|
|
13
|
+
INVALID_FIELD_NAME = 2001,
|
|
14
|
+
INVALID_FIELD_CHARS = 2002,
|
|
15
|
+
RESERVED_WORD_AS_FIELD = 2003,
|
|
16
|
+
INVALID_NUMERIC_VALUE = 3001,
|
|
17
|
+
INVALID_DATE_FORMAT = 3002,
|
|
18
|
+
INVALID_RANGE_FORMAT = 3003,
|
|
19
|
+
INVALID_RANGE_OPERATOR = 3004,
|
|
20
|
+
EXPECTED_RANGE_VALUE = 3005,
|
|
21
|
+
RANGE_START_GREATER_THAN_END = 3006,
|
|
22
|
+
WILDCARD_NOT_ALLOWED = 3007,
|
|
23
|
+
INVALID_WILDCARD_POSITION = 4001,
|
|
24
|
+
MULTIPLE_WILDCARDS = 4002,
|
|
25
|
+
EMPTY_IN_LIST = 5001,
|
|
26
|
+
INVALID_IN_VALUE = 5002,
|
|
27
|
+
EXPECTED_IN_SEPARATOR = 5003,
|
|
28
|
+
EXPECTED_LPAREN_AFTER_IN = 5004
|
|
29
|
+
}
|
|
2
30
|
export type ValidationError = {
|
|
3
31
|
message: string;
|
|
32
|
+
code: SearchQueryErrorCode;
|
|
33
|
+
value?: string;
|
|
4
34
|
position: number;
|
|
5
35
|
length: number;
|
|
6
36
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "search-input-query-parser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A parser for advanced search query syntax with field:value support",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"search",
|
|
@@ -54,9 +54,10 @@
|
|
|
54
54
|
"license": "ISC",
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/jest": "^29.5.14",
|
|
57
|
-
"@types/node": "^
|
|
57
|
+
"@types/node": "^22.10.1",
|
|
58
58
|
"jest": "^29.7.0",
|
|
59
|
+
"npm-check-updates": "^17.1.11",
|
|
59
60
|
"ts-jest": "^29.2.5",
|
|
60
|
-
"typescript": "^5.
|
|
61
|
+
"typescript": "^5.7.2"
|
|
61
62
|
}
|
|
62
|
-
}
|
|
63
|
+
}
|
package/src/first-pass-parser.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { TokenType, TokenStream, currentToken, advanceStream } from "./lexer";
|
|
2
2
|
import { parsePrimary } from "./parse-primary";
|
|
3
|
+
import { SearchQueryErrorCode } from "./validator";
|
|
3
4
|
|
|
4
5
|
// First Pass AST types (from tokenizer/parser)
|
|
5
6
|
export type PositionLength = {
|
|
@@ -94,6 +95,8 @@ export const parseExpression = (
|
|
|
94
95
|
if (nextToken.type === TokenType.EOF) {
|
|
95
96
|
throw {
|
|
96
97
|
message: `Unexpected token: ${token.value}`,
|
|
98
|
+
code: SearchQueryErrorCode.UNEXPECTED_TOKEN,
|
|
99
|
+
value: token.value,
|
|
97
100
|
position: token.position,
|
|
98
101
|
length: token.length,
|
|
99
102
|
};
|
package/src/lexer.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { SearchQueryErrorCode } from "./validator";
|
|
2
|
+
|
|
1
3
|
// Token types and data structures
|
|
2
4
|
export enum TokenType {
|
|
3
5
|
STRING = "STRING",
|
|
@@ -123,7 +125,12 @@ const tokenizeQuotedString = (
|
|
|
123
125
|
}
|
|
124
126
|
}
|
|
125
127
|
|
|
126
|
-
throw {
|
|
128
|
+
throw {
|
|
129
|
+
message: "Unterminated quoted string",
|
|
130
|
+
code: SearchQueryErrorCode.UNTERMINATED_QUOTED_STRING,
|
|
131
|
+
position,
|
|
132
|
+
length,
|
|
133
|
+
};
|
|
127
134
|
};
|
|
128
135
|
|
|
129
136
|
const tokenizeString = (input: string, position: number): [Token, number] => {
|
|
@@ -317,6 +324,7 @@ export const tokenize = (input: string): Token[] => {
|
|
|
317
324
|
throw {
|
|
318
325
|
message:
|
|
319
326
|
"Invalid syntax: Missing operator or whitespace between terms",
|
|
327
|
+
code: SearchQueryErrorCode.MISSING_OPERATOR_OR_WHITESPACE,
|
|
320
328
|
position: position,
|
|
321
329
|
length: 1,
|
|
322
330
|
};
|
|
@@ -333,6 +341,7 @@ export const tokenize = (input: string): Token[] => {
|
|
|
333
341
|
throw {
|
|
334
342
|
message:
|
|
335
343
|
"Invalid syntax: Missing operator or whitespace between terms",
|
|
344
|
+
code: SearchQueryErrorCode.MISSING_OPERATOR_OR_WHITESPACE,
|
|
336
345
|
position: newPos,
|
|
337
346
|
length: 1,
|
|
338
347
|
};
|
package/src/parse-in-values.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ParseResult } from "./first-pass-parser";
|
|
2
2
|
import { TokenStream, currentToken, TokenType, advanceStream } from "./lexer";
|
|
3
|
-
|
|
3
|
+
import { SearchQueryErrorCode } from "./validator";
|
|
4
4
|
|
|
5
5
|
export const parseInValues = (
|
|
6
6
|
stream: TokenStream,
|
|
@@ -13,6 +13,7 @@ export const parseInValues = (
|
|
|
13
13
|
if (currentToken(currentStream).type !== TokenType.LPAREN) {
|
|
14
14
|
throw {
|
|
15
15
|
message: "Expected '(' after IN",
|
|
16
|
+
code: SearchQueryErrorCode.EXPECTED_LPAREN_AFTER_IN,
|
|
16
17
|
position: inValuePosition, // Use the position passed from the caller
|
|
17
18
|
length: 1,
|
|
18
19
|
};
|
|
@@ -26,6 +27,7 @@ export const parseInValues = (
|
|
|
26
27
|
if (values.length === 0) {
|
|
27
28
|
throw {
|
|
28
29
|
message: "IN operator requires at least one value",
|
|
30
|
+
code: SearchQueryErrorCode.EMPTY_IN_LIST,
|
|
29
31
|
position: token.position,
|
|
30
32
|
length: 1,
|
|
31
33
|
};
|
|
@@ -43,6 +45,7 @@ export const parseInValues = (
|
|
|
43
45
|
token.type !== TokenType.COMMA)) {
|
|
44
46
|
throw {
|
|
45
47
|
message: "Expected ',' or ')' after IN value",
|
|
48
|
+
code: SearchQueryErrorCode.EXPECTED_IN_SEPARATOR,
|
|
46
49
|
position: token.position,
|
|
47
50
|
length: 1,
|
|
48
51
|
};
|
|
@@ -64,6 +67,7 @@ export const parseInValues = (
|
|
|
64
67
|
}
|
|
65
68
|
throw {
|
|
66
69
|
message: "Expected ',' or ')' after IN value",
|
|
70
|
+
code: SearchQueryErrorCode.EXPECTED_IN_SEPARATOR,
|
|
67
71
|
position: nextToken.position,
|
|
68
72
|
length: 1,
|
|
69
73
|
};
|
package/src/parse-primary.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ParseResult, FirstPassExpression, parseExpression } from "./first-pass-parser";
|
|
2
2
|
import { parseInValues } from "./parse-in-values";
|
|
3
3
|
import { TokenStream, currentToken, TokenType, advanceStream } from "./lexer";
|
|
4
|
+
import { SearchQueryErrorCode } from "./validator";
|
|
4
5
|
|
|
5
6
|
export const expectToken = (
|
|
6
7
|
stream: TokenStream,
|
|
@@ -11,6 +12,8 @@ export const expectToken = (
|
|
|
11
12
|
if (token.type !== type) {
|
|
12
13
|
throw {
|
|
13
14
|
message: message ? message : `Expected ${type}`,
|
|
15
|
+
code: SearchQueryErrorCode.EXPECTED_TOKEN,
|
|
16
|
+
value: type,
|
|
14
17
|
position: token.position,
|
|
15
18
|
length: token.length,
|
|
16
19
|
};
|
|
@@ -158,6 +161,8 @@ export const parsePrimary = (
|
|
|
158
161
|
case TokenType.OR:
|
|
159
162
|
throw {
|
|
160
163
|
message: `${token.value} is a reserved word`,
|
|
164
|
+
code: SearchQueryErrorCode.RESERVED_WORD,
|
|
165
|
+
value: token.value,
|
|
161
166
|
position: token.position,
|
|
162
167
|
length: token.length,
|
|
163
168
|
};
|
|
@@ -165,6 +170,7 @@ export const parsePrimary = (
|
|
|
165
170
|
case TokenType.RPAREN:
|
|
166
171
|
throw {
|
|
167
172
|
message: 'Unexpected ")"',
|
|
173
|
+
code: SearchQueryErrorCode.UNEXPECTED_RIGHT_PAREN,
|
|
168
174
|
position: token.position,
|
|
169
175
|
length: token.length,
|
|
170
176
|
};
|
|
@@ -172,6 +178,7 @@ export const parsePrimary = (
|
|
|
172
178
|
default:
|
|
173
179
|
throw {
|
|
174
180
|
message: "Unexpected token",
|
|
181
|
+
code: SearchQueryErrorCode.UNEXPECTED_TOKEN,
|
|
175
182
|
position: token.position,
|
|
176
183
|
length: token.length,
|
|
177
184
|
};
|