search-input-query-parser 0.1.1 → 0.1.3

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.
@@ -0,0 +1,320 @@
1
+ import {
2
+ Expression,
3
+ SearchQuery,
4
+ parseSearchInputQuery,
5
+ FieldSchema,
6
+ WildcardPattern,
7
+ } from "./parser";
8
+
9
+ export interface SqlQueryResult {
10
+ text: string;
11
+ values: any[];
12
+ }
13
+
14
+ interface SqlState {
15
+ paramCounter: number;
16
+ values: any[];
17
+ searchableColumns: string[];
18
+ schemas: Map<string, FieldSchema>;
19
+ language?: string; // For tsvector configuration
20
+ }
21
+
22
+
23
+ export interface SearchQueryOptions {
24
+ language?: string; // PostgreSQL language configuration for tsvector
25
+ }
26
+
27
+ // Constants
28
+ const SPECIAL_CHARS = ["%", "_"] as const;
29
+ const ESCAPE_CHAR = "\\";
30
+
31
+ // Helper Functions
32
+ const escapeRegExp = (str: string): string =>
33
+ str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
34
+
35
+ const stripQuotes = (value: string): string => {
36
+ if (value.startsWith('"') && value.endsWith('"')) {
37
+ return value.slice(1, -1);
38
+ }
39
+ return value;
40
+ };
41
+
42
+ const cleanQuotedString = (
43
+ value: string,
44
+ stripOuterQuotes: boolean = true
45
+ ): string => {
46
+ // First strip the outer quotes if requested
47
+ let cleaned = stripOuterQuotes ? stripQuotes(value) : value;
48
+
49
+ // Replace escaped quotes with regular quotes
50
+ cleaned = cleaned.replace(/\\"/g, '"');
51
+
52
+ // Clean up any remaining escape characters
53
+ cleaned = cleaned.replace(/\\\\/g, "\\");
54
+
55
+ return cleaned;
56
+ };
57
+
58
+ // Create a new parameter placeholder and update state
59
+ const nextParam = (state: SqlState): [string, SqlState] => {
60
+ const paramName = `$${state.paramCounter}`;
61
+ const newState = {
62
+ ...state,
63
+ paramCounter: state.paramCounter + 1,
64
+ };
65
+ return [paramName, newState];
66
+ };
67
+
68
+ // Add a value to the state and return updated state
69
+ const addValue = (state: SqlState, value: any): SqlState => ({
70
+ ...state,
71
+ values: [...state.values, value],
72
+ });
73
+
74
+ /**
75
+ * Convert a wildcard pattern to SQL
76
+ */
77
+ const wildcardPatternToSql = (
78
+ expr: WildcardPattern,
79
+ state: SqlState
80
+ ): [string, SqlState] => {
81
+ if (expr.prefix === "") {
82
+ return ["1=1", state];
83
+ }
84
+
85
+ const [paramName, newState] = nextParam(state);
86
+ const cleanedPrefix = cleanQuotedString(expr.prefix);
87
+
88
+ const langConfig = state.language || "english";
89
+ const conditions = state.searchableColumns.map(
90
+ (column) => `to_tsvector('${langConfig}', ${column})`
91
+ );
92
+ const tsvectorCondition = `(${conditions.join(
93
+ " || "
94
+ )}) @@ to_tsquery('${langConfig}', ${paramName})`;
95
+ return [tsvectorCondition, addValue(newState, `${cleanedPrefix}:*`)];
96
+ };
97
+
98
+ /**
99
+ * Convert a search term to SQL conditions based on search type
100
+ */
101
+ const searchTermToSql = (
102
+ value: string,
103
+ state: SqlState
104
+ ): [string, SqlState] => {
105
+ const [paramName, newState] = nextParam(state);
106
+ const hasWildcard = value.endsWith("*");
107
+ const cleanedValue = cleanQuotedString(value);
108
+ const baseValue = hasWildcard ? cleanedValue.slice(0, -1) : cleanedValue;
109
+
110
+ const langConfig = state.language || "english";
111
+ const conditions = state.searchableColumns.map(
112
+ (column) => `to_tsvector('${langConfig}', ${column})`
113
+ );
114
+ const tsvectorCondition = `(${conditions.join(" || ")}) @@ ${
115
+ hasWildcard ? "to_tsquery" : "plainto_tsquery"
116
+ }('${langConfig}', ${paramName})`;
117
+ return [
118
+ tsvectorCondition,
119
+ addValue(newState, hasWildcard ? `${baseValue}:*` : baseValue),
120
+ ];
121
+ };
122
+
123
+ /**
124
+ * Convert a field:value pair to SQL based on search type
125
+ */
126
+ const fieldValueToSql = (
127
+ field: string,
128
+ value: string,
129
+ state: SqlState
130
+ ): [string, SqlState] => {
131
+ const [paramName, newState] = nextParam(state);
132
+ const schema = state.schemas.get(field.toLowerCase());
133
+ const hasWildcard = value.endsWith("*");
134
+ const cleanedValue = cleanQuotedString(value);
135
+ const baseValue = hasWildcard ? cleanedValue.slice(0, -1) : cleanedValue;
136
+
137
+ // Rest of the function remains the same...
138
+ switch (schema?.type) {
139
+ case "date":
140
+ return [
141
+ `${field} = ${paramName}`,
142
+ addValue(newState, cleanedValue),
143
+ ];
144
+ case "number":
145
+ return [
146
+ `${field} = ${paramName}`,
147
+ addValue(newState, Number(cleanedValue)),
148
+ ];
149
+ default:
150
+ const langConfig = state.language || "english";
151
+ return [
152
+ `to_tsvector('${langConfig}', ${field}) @@ ${
153
+ hasWildcard ? "to_tsquery" : "plainto_tsquery"
154
+ }('${langConfig}', ${paramName})`,
155
+ addValue(newState, hasWildcard ? `${baseValue}:*` : baseValue),
156
+ ];
157
+ }
158
+ };
159
+
160
+ /**
161
+ * Convert a range expression to SQL
162
+ */
163
+ const rangeToSql = (
164
+ field: string,
165
+ operator: string,
166
+ value: string,
167
+ value2: string | undefined,
168
+ state: SqlState
169
+ ): [string, SqlState] => {
170
+ const schema = state.schemas.get(field.toLowerCase());
171
+ const isDateField = schema?.type === "date";
172
+
173
+ if (operator === "BETWEEN" && value2) {
174
+ const [param1, state1] = nextParam(state);
175
+ const [param2, state2] = nextParam(state1);
176
+ let val1 = isDateField ? value : Number(value);
177
+ let val2 = isDateField ? value2 : Number(value2);
178
+ return [
179
+ `${field} BETWEEN ${param1} AND ${param2}`,
180
+ addValue(addValue(state2, val1), val2),
181
+ ];
182
+ }
183
+
184
+ const [paramName, newState] = nextParam(state);
185
+ const val = isDateField ? value : Number(value);
186
+ return [
187
+ `${field} ${operator} ${paramName}`,
188
+ addValue(newState, val),
189
+ ];
190
+ };
191
+
192
+ const inExpressionToSql = (
193
+ field: string,
194
+ values: string[],
195
+ state: SqlState
196
+ ): [string, SqlState] => {
197
+ let currentState = state;
198
+ const cleanedValues = values.map((v) => cleanQuotedString(v));
199
+
200
+ const paramNames: string[] = [];
201
+ const schema = state.schemas.get(field.toLowerCase());
202
+
203
+ for (const value of cleanedValues) {
204
+ const [paramName, newState] = nextParam(currentState);
205
+ paramNames.push(paramName);
206
+ currentState = addValue(
207
+ newState,
208
+ schema?.type === "number" ? Number(value) : value
209
+ );
210
+ }
211
+
212
+ return [
213
+ `${field} IN (${paramNames
214
+ .map((p) => p)
215
+ .join(", ")})`,
216
+ currentState,
217
+ ];
218
+ };
219
+
220
+ /**
221
+ * Convert a binary operation (AND/OR) to SQL
222
+ */
223
+ const binaryOpToSql = (
224
+ operator: string,
225
+ left: Expression,
226
+ right: Expression,
227
+ state: SqlState
228
+ ): [string, SqlState] => {
229
+ const [leftText, leftState] = expressionToSql(left, state);
230
+ const [rightText, rightState] = expressionToSql(right, leftState);
231
+ return [`(${leftText} ${operator} ${rightText})`, rightState];
232
+ };
233
+
234
+ /**
235
+ * Convert a single expression to SQL
236
+ */
237
+ const expressionToSql = (
238
+ expr: Expression,
239
+ state: SqlState
240
+ ): [string, SqlState] => {
241
+ switch (expr.type) {
242
+ case "SEARCH_TERM":
243
+ return searchTermToSql(expr.value, state);
244
+
245
+ case "WILDCARD":
246
+ return wildcardPatternToSql(expr, state);
247
+
248
+ case "IN":
249
+ return inExpressionToSql(
250
+ expr.field.value,
251
+ expr.values.map((v) => v.value),
252
+ state
253
+ );
254
+
255
+ case "FIELD_VALUE":
256
+ return fieldValueToSql(expr.field.value, expr.value.value, state);
257
+
258
+ case "RANGE":
259
+ return rangeToSql(
260
+ expr.field.value,
261
+ expr.operator,
262
+ expr.value.value,
263
+ expr.value2?.value,
264
+ state
265
+ );
266
+
267
+ case "AND":
268
+ return binaryOpToSql("AND", expr.left, expr.right, state);
269
+
270
+ case "OR":
271
+ return binaryOpToSql("OR", expr.left, expr.right, state);
272
+
273
+ case "NOT": {
274
+ const [sqlText, newState] = expressionToSql(expr.expression, state);
275
+ return [`NOT ${sqlText}`, newState];
276
+ }
277
+ }
278
+ };
279
+
280
+ /**
281
+ * Convert a SearchQuery to a SQL WHERE clause with specified search type
282
+ */
283
+ export const searchQueryToTsVectorSql = (
284
+ query: SearchQuery,
285
+ searchableColumns: string[],
286
+ schemas: FieldSchema[] = [],
287
+ options: SearchQueryOptions = {}
288
+ ): SqlQueryResult => {
289
+ const initialState: SqlState = {
290
+ paramCounter: 1,
291
+ values: [],
292
+ searchableColumns,
293
+ schemas: new Map(schemas.map((s) => [s.name.toLowerCase(), s])),
294
+ language: options.language,
295
+ };
296
+
297
+ if (!query.expression) {
298
+ return { text: "1=1", values: [] };
299
+ }
300
+
301
+ const [text, finalState] = expressionToSql(query.expression, initialState);
302
+ return { text, values: finalState.values };
303
+ };
304
+
305
+ /**
306
+ * Convert a search string directly to SQL
307
+ */
308
+ export const searchStringToTsVectorSql = (
309
+ searchString: string,
310
+ searchableColumns: string[],
311
+ schemas: FieldSchema[] = [],
312
+ options: SearchQueryOptions = {}
313
+ ): SqlQueryResult => {
314
+ const query = parseSearchInputQuery(searchString, schemas);
315
+ if (query.type === "SEARCH_QUERY_ERROR") {
316
+ throw new Error(`Parse error: ${query.errors[0].message}`);
317
+ }
318
+
319
+ return searchQueryToTsVectorSql(query, searchableColumns, schemas, options);
320
+ };