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
package/src/parser.ts ADDED
@@ -0,0 +1,219 @@
1
+ import { tokenize, createStream, currentToken, TokenType } from "./lexer";
2
+ import {
3
+ parseExpression,
4
+ PositionLength,
5
+ WildcardPattern as FirstPassWildcard,
6
+ } from "./first-pass-parser";
7
+ import { validateSearchQuery, ValidationError } from "./validator";
8
+ import { validateExpressionFields } from "./validate-expression-fields";
9
+ import { transformToExpression } from "./transform-to-expression";
10
+
11
+ // Schema types for range queries
12
+ interface FieldSchema {
13
+ name: string;
14
+ type: "string" | "number" | "date" | "boolean";
15
+ }
16
+
17
+ // Second Pass AST types (semantic analysis)
18
+ type SearchTerm = {
19
+ readonly type: "SEARCH_TERM";
20
+ readonly value: string;
21
+ } & PositionLength;
22
+
23
+ type WildcardPattern = {
24
+ readonly type: "WILDCARD";
25
+ readonly prefix: string;
26
+ readonly quoted: boolean;
27
+ } & PositionLength;
28
+
29
+ type Field = {
30
+ readonly type: "FIELD";
31
+ readonly value: string;
32
+ } & PositionLength;
33
+
34
+ type Value = {
35
+ readonly type: "VALUE";
36
+ readonly value: string;
37
+ } & PositionLength;
38
+
39
+ type RangeOperator = ">=" | ">" | "<=" | "<" | "BETWEEN";
40
+
41
+ type RangeExpression = {
42
+ readonly type: "RANGE";
43
+ readonly field: Field;
44
+ readonly operator: RangeOperator;
45
+ readonly value: Value;
46
+ readonly value2?: Value; // For BETWEEN
47
+ } & PositionLength;
48
+
49
+ export type FieldValue = {
50
+ readonly type: "FIELD_VALUE";
51
+ readonly field: Field;
52
+ readonly value: Value;
53
+ };
54
+
55
+ type And = {
56
+ readonly type: "AND";
57
+ readonly left: Expression;
58
+ readonly right: Expression;
59
+ } & PositionLength;
60
+
61
+ type Or = {
62
+ readonly type: "OR";
63
+ readonly left: Expression;
64
+ readonly right: Expression;
65
+ } & PositionLength;
66
+
67
+ type Not = {
68
+ readonly type: "NOT";
69
+ readonly expression: Expression;
70
+ } & PositionLength;
71
+
72
+ type InExpression = {
73
+ readonly type: "IN";
74
+ readonly field: Field;
75
+ readonly values: Value[];
76
+ } & PositionLength;
77
+
78
+ type Expression =
79
+ | SearchTerm
80
+ | WildcardPattern
81
+ | FieldValue
82
+ | RangeExpression
83
+ | And
84
+ | Or
85
+ | Not
86
+ | InExpression;
87
+
88
+ type SearchQuery = {
89
+ readonly type: "SEARCH_QUERY";
90
+ readonly expression: Expression | null;
91
+ };
92
+
93
+ type SearchQueryError = {
94
+ readonly type: "SEARCH_QUERY_ERROR";
95
+ readonly expression: null;
96
+ readonly errors: ValidationError[];
97
+ };
98
+
99
+ // Helper function to stringify expressions
100
+ const stringify = (expr: Expression): string => {
101
+ switch (expr.type) {
102
+ case "SEARCH_TERM":
103
+ return expr.value;
104
+ case "WILDCARD":
105
+ return `${expr.prefix}*`;
106
+ case "FIELD_VALUE":
107
+ return `${expr.field.value}:${expr.value.value}`;
108
+ case "RANGE":
109
+ if (expr.operator === "BETWEEN") {
110
+ return `${expr.field.value}:${expr.value.value}..${expr.value2?.value}`;
111
+ }
112
+ return `${expr.field.value}:${expr.operator}${expr.value.value}`;
113
+ case "NOT":
114
+ return `NOT (${stringify(expr.expression)})`;
115
+ case "AND":
116
+ return `(${stringify(expr.left)} AND ${stringify(expr.right)})`;
117
+ case "OR":
118
+ return `(${stringify(expr.left)} OR ${stringify(expr.right)})`;
119
+ case "IN": {
120
+ const values = expr.values.map((v: { value: string }) => v.value).join(",");
121
+ return `${expr.field.value}:IN(${values})`;
122
+ }
123
+ }
124
+ };
125
+
126
+ // Main parse function
127
+ export const parseSearchInputQuery = (
128
+ input: string,
129
+ fieldSchemas: FieldSchema[] = []
130
+ ): SearchQuery | SearchQueryError => {
131
+ try {
132
+ const tokens = tokenize(input);
133
+ const stream = createStream(tokens);
134
+
135
+ if (currentToken(stream).type === TokenType.EOF) {
136
+ return { type: "SEARCH_QUERY", expression: null };
137
+ }
138
+
139
+ const result = parseExpression(stream);
140
+
141
+ const finalToken = currentToken(result.stream);
142
+ if (finalToken.type !== TokenType.EOF) {
143
+ throw {
144
+ message: 'Unexpected ")"',
145
+ position: finalToken.position,
146
+ length: finalToken.length,
147
+ };
148
+ }
149
+
150
+ const errors = validateSearchQuery(result.result);
151
+ const fieldErrors: ValidationError[] = [];
152
+
153
+ const allowedFields = fieldSchemas.map((s) => s.name.toLowerCase());
154
+
155
+ if (allowedFields.length > 0) {
156
+ const columnSet = new Set(allowedFields.map((col) => col.toLowerCase()));
157
+ const schemaMap = new Map(
158
+ fieldSchemas.map((s) => [s.name.toLowerCase(), s])
159
+ );
160
+ validateExpressionFields(
161
+ result.result,
162
+ columnSet,
163
+ fieldErrors,
164
+ schemaMap
165
+ );
166
+ }
167
+
168
+ const fieldErrorKeys = fieldErrors.map(
169
+ ({ position, length }) => `${position}-${length}`
170
+ );
171
+ const errorsToRemove = errors.filter(({ position, length }) =>
172
+ fieldErrorKeys.includes(`${position}-${length}`)
173
+ );
174
+ const fieldErrorsFiltered = fieldErrors.filter(
175
+ ({ position, length }) =>
176
+ !errorsToRemove.some(
177
+ (error) => error.position === position && error.length === length
178
+ )
179
+ );
180
+
181
+ const allErrors = [...errors, ...fieldErrorsFiltered].sort(
182
+ (a, b) => a.position - b.position
183
+ );
184
+
185
+ if (allErrors.length > 0) {
186
+ return {
187
+ type: "SEARCH_QUERY_ERROR",
188
+ expression: null,
189
+ errors: allErrors,
190
+ };
191
+ }
192
+
193
+ const schemaMap = new Map(
194
+ fieldSchemas.map((s) => [s.name.toLowerCase(), s])
195
+ );
196
+ const expression = transformToExpression(result.result, schemaMap);
197
+
198
+ return { type: "SEARCH_QUERY", expression };
199
+ } catch (error: any) {
200
+ return {
201
+ type: "SEARCH_QUERY_ERROR",
202
+ expression: null,
203
+ errors: [error],
204
+ };
205
+ }
206
+ };
207
+
208
+ export {
209
+ type SearchQuery,
210
+ type SearchQueryError,
211
+ type Expression,
212
+ type ValidationError,
213
+ type FieldSchema,
214
+ type RangeOperator,
215
+ type RangeExpression,
216
+ type WildcardPattern,
217
+ type Value,
218
+ stringify,
219
+ };