@gblikas/querykit 0.1.0 → 0.3.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/.cursor/BUGBOT.md CHANGED
@@ -1,21 +1,84 @@
1
1
  # Project review guidelines
2
2
 
3
- Bellow is a list of generally accepted best-practices to prevent bugs in projects. Not all guidelines may apply to the current project; please make sure to read any README.md files in order to correlate goals for bug detection.
3
+ Below is a list of generally accepted best-practices to prevent bugs in QueryKit. Not all guidelines may apply to every component; please make sure to read the README.md for context on the project's goals.
4
4
 
5
5
  ## Security focus areas
6
6
 
7
7
  - Validate user input in API endpoints
8
8
  - Check for SQL injection vulnerabilities in database queries
9
9
  - Ensure proper authentication on protected routes
10
+ - Validate query inputs using `parseWithContext` with security options
11
+ - Use `allowedFields` and `denyFields` to restrict queryable fields
12
+ - Set `maxQueryDepth` and `maxClauseCount` to prevent DoS attacks
13
+
14
+ ### Query parsing security
15
+
16
+ When using the input parser or `parseWithContext`:
17
+
18
+ 1. **Never trust user-provided queries** - Always validate with security options:
19
+ ```typescript
20
+ const result = parser.parseWithContext(userQuery, {
21
+ securityOptions: {
22
+ allowedFields: ['name', 'status', 'priority'],
23
+ denyFields: ['password', 'secret'],
24
+ maxQueryDepth: 5,
25
+ maxClauseCount: 20
26
+ }
27
+ });
28
+
29
+ if (!result.security?.passed) {
30
+ // Reject query - contains violations
31
+ }
32
+ ```
33
+
34
+ 2. **Schema validation** - Use schema to detect typos and invalid fields early:
35
+ ```typescript
36
+ const result = parser.parseWithContext(userQuery, { schema });
37
+ if (!result.fieldValidation?.valid) {
38
+ // Show user-friendly error with suggestions
39
+ }
40
+ ```
41
+
42
+ 3. **Input parser limitations** - The input parser (`parseQueryInput`, `parseQueryTokens`) is regex-based for performance. It may accept inputs that the main parser rejects. Always validate with `parseWithContext` or `parser.parse()` before executing queries.
10
43
 
11
44
  ## Architecture patterns
12
45
 
13
46
  - Use dependency injection for services
14
47
  - Follow the repository pattern for data access
15
48
  - Implement proper error handling with custom error classes
49
+ - Parser components follow Single Responsibility Principle:
50
+ - `input-parser.ts` - Fast, regex-based tokenization for UI feedback
51
+ - `parser.ts` - Full Liqe-based parsing with AST generation
52
+ - `parseWithContext` - Orchestrates both for rich context
53
+
54
+ ### Parser architecture
55
+
56
+ The parsing system has two tiers:
57
+
58
+ 1. **Input Parser** (`parseQueryInput`, `parseQueryTokens`)
59
+ - Purpose: Real-time UI feedback (highlighting, cursor context)
60
+ - Performance: O(n) regex-based, no AST generation
61
+ - Error handling: Best-effort, never throws
62
+ - Use for: Search bar highlighting, autocomplete triggering
63
+
64
+ 2. **Query Parser** (`parser.parse`, `parseWithContext`)
65
+ - Purpose: Query validation and execution
66
+ - Performance: Full Liqe grammar parsing
67
+ - Error handling: Strict validation, detailed error messages
68
+ - Use for: Query execution, security validation
16
69
 
17
70
  ## Common issues
18
71
 
19
72
  - Memory leaks in React components (check useEffect cleanup)
20
73
  - Missing error boundaries in UI components
21
- - Inconsistent naming conventions (use camelCase for functions)
74
+ - Inconsistent naming conventions (use camelCase for functions)
75
+ - Not checking `result.success` before accessing `result.ast`
76
+ - Using input parser for security validation (use `parseWithContext` instead)
77
+ - Forgetting to provide `cursorPosition` when autocomplete is needed
78
+
79
+ ## Testing guidelines
80
+
81
+ - All parser features require co-located tests
82
+ - Use divergence tests to document differences between input parser and main parser
83
+ - Token consistency tests verify `parseWithContext` tokens match `parseQueryTokens`
84
+ - Security tests should cover field restrictions, depth limits, and value sanitization
package/README.md CHANGED
@@ -275,6 +275,165 @@ const publicSearchKit = createQueryKit({
275
275
  });
276
276
  ```
277
277
 
278
+ ## Input Parsing for Search UIs
279
+
280
+ QueryKit provides utilities for building rich search bar experiences with real-time feedback, including key:value highlighting, autocomplete suggestions, and error recovery hints.
281
+
282
+ ### Real-Time Token Parsing
283
+
284
+ Use `parseQueryInput` and `parseQueryTokens` for lightweight, real-time parsing as users type:
285
+
286
+ ```typescript
287
+ import { parseQueryInput, parseQueryTokens } from '@gblikas/querykit';
288
+
289
+ // Parse input to get terms and cursor context
290
+ const input = 'status:done AND priority:';
291
+ const result = parseQueryInput(input, { cursorPosition: 25 });
292
+
293
+ // result.terms contains parsed terms:
294
+ // [{ key: 'status', value: 'done', ... }, { key: 'priority', value: null, ... }]
295
+
296
+ // result.cursorContext tells you where the cursor is: 'key', 'value', or 'operator'
297
+ console.log(result.cursorContext); // 'value' (cursor is after 'priority:')
298
+
299
+ // Get interleaved tokens (terms + operators) for highlighting
300
+ const tokens = parseQueryTokens(input);
301
+ // [
302
+ // { type: 'term', key: 'status', value: 'done', startPosition: 0, endPosition: 11 },
303
+ // { type: 'operator', operator: 'AND', startPosition: 12, endPosition: 15 },
304
+ // { type: 'term', key: 'priority', value: null, startPosition: 16, endPosition: 25 }
305
+ // ]
306
+ ```
307
+
308
+ ### Rich Context with parseWithContext
309
+
310
+ For comprehensive parsing with schema validation, autocomplete, and error recovery:
311
+
312
+ ```typescript
313
+ import { QueryParser } from '@gblikas/querykit';
314
+
315
+ const parser = new QueryParser();
316
+
317
+ // Define your schema for validation and autocomplete
318
+ const schema = {
319
+ status: {
320
+ type: 'string',
321
+ allowedValues: ['todo', 'doing', 'done'],
322
+ description: 'Task status'
323
+ },
324
+ priority: { type: 'number', description: 'Priority level (1-5)' },
325
+ assignee: { type: 'string', description: 'Assigned user' }
326
+ };
327
+
328
+ const result = parser.parseWithContext('status:do', {
329
+ cursorPosition: 9,
330
+ schema,
331
+ securityOptions: { maxClauseCount: 10 }
332
+ });
333
+
334
+ // Always returns a result object (never throws)
335
+ console.log(result.success); // true/false - whether parsing succeeded
336
+ console.log(result.tokens); // Tokenized input (always available)
337
+ console.log(result.structure); // Query structure analysis
338
+ console.log(result.ast); // AST (if successful)
339
+ console.log(result.error); // Error details (if failed)
340
+
341
+ // Autocomplete suggestions based on cursor position
342
+ console.log(result.suggestions);
343
+ // {
344
+ // context: 'value',
345
+ // currentField: 'status',
346
+ // values: [
347
+ // { value: 'doing', score: 80 },
348
+ // { value: 'done', score: 80 }
349
+ // ]
350
+ // }
351
+
352
+ // Schema validation results
353
+ console.log(result.fieldValidation);
354
+ // { valid: true, fields: [...], unknownFields: [] }
355
+
356
+ // Security pre-check
357
+ console.log(result.security);
358
+ // { passed: true, violations: [], warnings: [] }
359
+ ```
360
+
361
+ ### Error Recovery
362
+
363
+ When parsing fails, `parseWithContext` provides helpful recovery hints:
364
+
365
+ ```typescript
366
+ const result = parser.parseWithContext('status:"incomplete');
367
+
368
+ console.log(result.recovery);
369
+ // {
370
+ // issue: 'unclosed_quote',
371
+ // message: 'Unclosed double quote detected',
372
+ // suggestion: 'Add a closing " to complete the quoted value',
373
+ // autofix: 'status:"incomplete"',
374
+ // position: 7
375
+ // }
376
+ ```
377
+
378
+ Error types detected:
379
+ - `unclosed_quote` - Missing closing quote (with autofix)
380
+ - `unclosed_parenthesis` - Unbalanced parentheses (with autofix)
381
+ - `trailing_operator` - Query ends with AND/OR/NOT (with autofix)
382
+ - `missing_value` - Field has colon but no value
383
+ - `syntax_error` - Generic syntax issue
384
+
385
+ ### Building a Search Bar with Highlighting
386
+
387
+ Here's a React example using the input parser for highlighting:
388
+
389
+ ```tsx
390
+ import { parseQueryTokens } from '@gblikas/querykit';
391
+
392
+ function SearchBar({ value, onChange }) {
393
+ const tokens = parseQueryTokens(value);
394
+
395
+ const renderHighlightedQuery = () => {
396
+ if (!value) return null;
397
+
398
+ return tokens.map((token, idx) => {
399
+ const text = value.slice(token.startPosition, token.endPosition);
400
+
401
+ if (token.type === 'operator') {
402
+ return <span key={idx} className="text-purple-500">{text}</span>;
403
+ }
404
+
405
+ // Term token - highlight key and value differently
406
+ if (token.key && token.operator) {
407
+ const keyEnd = token.startPosition + token.key.length;
408
+ const opEnd = keyEnd + token.operator.length;
409
+ return (
410
+ <span key={idx}>
411
+ <span className="text-orange-400">{token.key}</span>
412
+ <span className="text-gray-500">{token.operator}</span>
413
+ <span className="text-blue-400">{value.slice(opEnd, token.endPosition)}</span>
414
+ </span>
415
+ );
416
+ }
417
+
418
+ return <span key={idx}>{text}</span>;
419
+ });
420
+ };
421
+
422
+ return (
423
+ <div className="relative">
424
+ <div className="absolute inset-0 pointer-events-none">
425
+ {renderHighlightedQuery()}
426
+ </div>
427
+ <input
428
+ value={value}
429
+ onChange={(e) => onChange(e.target.value)}
430
+ className="bg-transparent text-transparent caret-black"
431
+ />
432
+ </div>
433
+ );
434
+ }
435
+ ```
436
+
278
437
  ## Roadmap
279
438
 
280
439
  ### Core Parsing Engine and DSL
@@ -283,6 +442,9 @@ const publicSearchKit = createQueryKit({
283
442
  - [x] Develop internal AST representation
284
443
  - [x] Implement consistent syntax for logical operators (AND, OR, NOT)
285
444
  - [x] Support standard comparison operators (==, !=, >, >=, <, <=)
445
+ - [x] Real-time input parsing for search UIs
446
+ - [x] Autocomplete suggestions with schema awareness
447
+ - [x] Error recovery hints with autofix
286
448
 
287
449
  ### First Adapters
288
450
  - [x] Drizzle ORM integration
@@ -299,7 +461,7 @@ const publicSearchKit = createQueryKit({
299
461
  - [ ] Pagination helpers
300
462
 
301
463
  ### Ecosystem Expansion
302
- - [ ] Frontend query builder components
464
+ - [x] Frontend query builder components (input parser)
303
465
  - [ ] Additional ORM adapters
304
466
  - [ ] Server middleware for Express/Fastify
305
467
  - [ ] TypeScript SDK generation
@@ -1,2 +1,3 @@
1
1
  export * from './types';
2
2
  export * from './parser';
3
+ export * from './input-parser';
@@ -16,3 +16,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./types"), exports);
18
18
  __exportStar(require("./parser"), exports);
19
+ __exportStar(require("./input-parser"), exports);
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Input Parser for QueryKit
3
+ *
4
+ * This module provides utilities for parsing partial/in-progress query input
5
+ * from search bars, enabling features like:
6
+ * - Key-value highlighting
7
+ * - Autocomplete suggestions
8
+ * - Real-time validation feedback
9
+ */
10
+ /**
11
+ * Represents the context of where the cursor is within a query term
12
+ */
13
+ export type CursorContext = 'key' | 'operator' | 'value' | 'empty' | 'between';
14
+ /**
15
+ * Represents the parsed context of a single query term
16
+ */
17
+ export interface IQueryInputTerm {
18
+ /**
19
+ * The field/key being typed (e.g., "status" in "status:done")
20
+ * Will be null if only a bare value is being typed
21
+ */
22
+ key: string | null;
23
+ /**
24
+ * The operator being used (e.g., ":", ">", ">=", "<", "<=", "!=")
25
+ * Will be null if no operator has been typed yet
26
+ */
27
+ operator: string | null;
28
+ /**
29
+ * The value being typed (e.g., "done" in "status:done")
30
+ * Will be null if no value has been typed yet
31
+ */
32
+ value: string | null;
33
+ /**
34
+ * The start position of this term in the original input string
35
+ */
36
+ startPosition: number;
37
+ /**
38
+ * The end position of this term in the original input string
39
+ */
40
+ endPosition: number;
41
+ /**
42
+ * The original raw text of this term
43
+ */
44
+ raw: string;
45
+ }
46
+ /**
47
+ * Represents the result of parsing query input
48
+ */
49
+ export interface IQueryInputContext {
50
+ /**
51
+ * All terms found in the input
52
+ */
53
+ terms: IQueryInputTerm[];
54
+ /**
55
+ * The term where the cursor is currently positioned (if cursorPosition was provided)
56
+ * Will be null if cursor is not within any term
57
+ */
58
+ activeTerm: IQueryInputTerm | null;
59
+ /**
60
+ * Where the cursor is within the active term
61
+ */
62
+ cursorContext: CursorContext;
63
+ /**
64
+ * The original input string
65
+ */
66
+ input: string;
67
+ /**
68
+ * The cursor position (if provided)
69
+ */
70
+ cursorPosition: number | null;
71
+ /**
72
+ * Logical operators found between terms (AND, OR, NOT)
73
+ */
74
+ logicalOperators: Array<{
75
+ operator: string;
76
+ position: number;
77
+ }>;
78
+ }
79
+ /**
80
+ * Options for parsing query input
81
+ */
82
+ export interface IQueryInputParserOptions {
83
+ /**
84
+ * Whether to treat the input as case-insensitive for keys
85
+ * @default false
86
+ */
87
+ caseInsensitiveKeys?: boolean;
88
+ }
89
+ /**
90
+ * Parse query input to extract structured information about the current search state.
91
+ *
92
+ * This function is designed for real-time parsing of user input in a search bar,
93
+ * allowing developers to:
94
+ * - Highlight keys and values differently
95
+ * - Provide autocomplete suggestions based on context
96
+ * - Validate input as the user types
97
+ *
98
+ * @param input The current search input string
99
+ * @param cursorPosition Optional cursor position to determine the active term
100
+ * @param options Optional parsing options
101
+ * @returns Structured information about the query input
102
+ *
103
+ * @example
104
+ * ```typescript
105
+ * // User is typing "status:d" (intending to type "status:done")
106
+ * const result = parseQueryInput('status:d');
107
+ * // result.terms[0] = { key: 'status', operator: ':', value: 'd', ... }
108
+ * // result.activeTerm = { key: 'status', operator: ':', value: 'd', ... }
109
+ * // result.cursorContext = 'value'
110
+ *
111
+ * // User is typing "priority:>2 status:"
112
+ * const result = parseQueryInput('priority:>2 status:', 19);
113
+ * // result.terms[0] = { key: 'priority', operator: ':>', value: '2', ... }
114
+ * // result.terms[1] = { key: 'status', operator: ':', value: null, ... }
115
+ * // result.activeTerm = result.terms[1] (cursor is at position 19)
116
+ * // result.cursorContext = 'value' (waiting for value input)
117
+ * ```
118
+ */
119
+ export declare function parseQueryInput(input: string, cursorPosition?: number, options?: IQueryInputParserOptions): IQueryInputContext;
120
+ /**
121
+ * Get the term at a specific cursor position.
122
+ * Convenience function for quick lookups.
123
+ *
124
+ * @param input The query input string
125
+ * @param cursorPosition The cursor position
126
+ * @returns The term at the cursor position, or null if none
127
+ */
128
+ export declare function getTermAtPosition(input: string, cursorPosition: number): IQueryInputTerm | null;
129
+ /**
130
+ * Check if the input appears to be a complete, valid query expression.
131
+ * This is a lightweight check - it doesn't guarantee the query will parse successfully.
132
+ *
133
+ * @param input The query input string
134
+ * @returns true if the input appears complete, false if it looks incomplete
135
+ */
136
+ export declare function isInputComplete(input: string): boolean;
137
+ /**
138
+ * Extract just the key and value from a simple input.
139
+ * Convenience function for the most common use case.
140
+ *
141
+ * @param input The query input string (e.g., "status:done")
142
+ * @returns Object with key and value, or null if not a key:value pattern
143
+ *
144
+ * @example
145
+ * ```typescript
146
+ * extractKeyValue('status:done');
147
+ * // { key: 'status', value: 'done' }
148
+ *
149
+ * extractKeyValue('status:');
150
+ * // { key: 'status', value: null }
151
+ *
152
+ * extractKeyValue('hello');
153
+ * // null (no key:value pattern)
154
+ * ```
155
+ */
156
+ export declare function extractKeyValue(input: string): {
157
+ key: string;
158
+ value: string | null;
159
+ } | null;
160
+ import type { IQueryToken } from './types';
161
+ /**
162
+ * A token in the query sequence - either a term or a logical operator
163
+ * This is an alias for IQueryToken from types.ts
164
+ */
165
+ export type QueryToken = IQueryToken;
166
+ /**
167
+ * Result of parsing query input into an interleaved token sequence
168
+ */
169
+ export interface IQueryTokenSequence {
170
+ /**
171
+ * Ordered sequence of tokens (terms and operators interleaved)
172
+ */
173
+ tokens: QueryToken[];
174
+ /**
175
+ * The original input string
176
+ */
177
+ input: string;
178
+ /**
179
+ * The token where the cursor is currently positioned (if cursorPosition was provided)
180
+ */
181
+ activeToken: QueryToken | null;
182
+ /**
183
+ * Index of the active token in the tokens array (-1 if none)
184
+ */
185
+ activeTokenIndex: number;
186
+ }
187
+ /**
188
+ * Parse query input into an interleaved sequence of terms and operators.
189
+ *
190
+ * This provides a flat, ordered representation ideal for:
191
+ * - Rendering query tokens as UI chips/tags
192
+ * - Building visual query builders
193
+ * - Syntax highlighting with proper ordering
194
+ *
195
+ * @param input The query input string
196
+ * @param cursorPosition Optional cursor position to identify active token
197
+ * @returns Ordered sequence of term and operator tokens
198
+ *
199
+ * @example
200
+ * ```typescript
201
+ * const result = parseQueryTokens('status:done AND priority:high');
202
+ * // result.tokens = [
203
+ * // { type: 'term', key: 'status', value: 'done', ... },
204
+ * // { type: 'operator', operator: 'AND', ... },
205
+ * // { type: 'term', key: 'priority', value: 'high', ... }
206
+ * // ]
207
+ *
208
+ * // For incomplete input like 'status:d'
209
+ * const result = parseQueryTokens('status:d');
210
+ * // result.tokens = [
211
+ * // { type: 'term', key: 'status', value: 'd', ... }
212
+ * // ]
213
+ * ```
214
+ */
215
+ export declare function parseQueryTokens(input: string, cursorPosition?: number): IQueryTokenSequence;