@gblikas/querykit 0.2.0 → 0.4.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 (39) hide show
  1. package/.cursor/BUGBOT.md +65 -2
  2. package/.husky/pre-commit +3 -3
  3. package/README.md +510 -1
  4. package/dist/index.d.ts +36 -3
  5. package/dist/index.js +20 -3
  6. package/dist/parser/index.d.ts +1 -0
  7. package/dist/parser/index.js +1 -0
  8. package/dist/parser/input-parser.d.ts +215 -0
  9. package/dist/parser/input-parser.js +493 -0
  10. package/dist/parser/parser.d.ts +114 -1
  11. package/dist/parser/parser.js +716 -0
  12. package/dist/parser/types.d.ts +432 -0
  13. package/dist/virtual-fields/index.d.ts +5 -0
  14. package/dist/virtual-fields/index.js +21 -0
  15. package/dist/virtual-fields/resolver.d.ts +17 -0
  16. package/dist/virtual-fields/resolver.js +107 -0
  17. package/dist/virtual-fields/types.d.ts +160 -0
  18. package/dist/virtual-fields/types.js +5 -0
  19. package/examples/qk-next/app/page.tsx +190 -86
  20. package/examples/qk-next/package.json +1 -1
  21. package/package.json +2 -2
  22. package/src/adapters/drizzle/index.ts +3 -3
  23. package/src/index.ts +77 -8
  24. package/src/parser/divergence.test.ts +357 -0
  25. package/src/parser/index.ts +2 -1
  26. package/src/parser/input-parser.test.ts +770 -0
  27. package/src/parser/input-parser.ts +697 -0
  28. package/src/parser/parse-with-context-suggestions.test.ts +360 -0
  29. package/src/parser/parse-with-context-validation.test.ts +447 -0
  30. package/src/parser/parse-with-context.test.ts +325 -0
  31. package/src/parser/parser.ts +872 -0
  32. package/src/parser/token-consistency.test.ts +341 -0
  33. package/src/parser/types.ts +545 -23
  34. package/src/virtual-fields/index.ts +6 -0
  35. package/src/virtual-fields/integration.test.ts +338 -0
  36. package/src/virtual-fields/resolver.ts +165 -0
  37. package/src/virtual-fields/types.ts +203 -0
  38. package/src/virtual-fields/virtual-fields.test.ts +831 -0
  39. package/examples/qk-next/pnpm-lock.yaml +0 -5623
package/dist/index.d.ts CHANGED
@@ -10,9 +10,11 @@ import { QueryParser, IParserOptions } from './parser';
10
10
  import { SqlTranslator } from './translators/sql';
11
11
  import { ISecurityOptions } from './security';
12
12
  import { IAdapter, IAdapterOptions } from './adapters';
13
+ import { IQueryContext, VirtualFieldsConfig } from './virtual-fields';
13
14
  export { QueryParser, IParserOptions, QueryBuilder, IQueryBuilderOptions, SqlTranslator };
14
15
  export * from './translators';
15
16
  export * from './adapters';
17
+ export * from './virtual-fields';
16
18
  /**
17
19
  * Create a new QueryBuilder instance
18
20
  */
@@ -24,7 +26,7 @@ export declare function createQueryParser(options?: IParserOptions): QueryParser
24
26
  /**
25
27
  * Options for creating a new QueryKit instance
26
28
  */
27
- export interface IQueryKitOptions<TSchema extends Record<string, object> = Record<string, Record<string, unknown>>> {
29
+ export interface IQueryKitOptions<TSchema extends Record<string, object> = Record<string, Record<string, unknown>>, TContext extends IQueryContext = IQueryContext> {
28
30
  /**
29
31
  * The adapter to use for database connections
30
32
  */
@@ -43,6 +45,37 @@ export interface IQueryKitOptions<TSchema extends Record<string, object> = Recor
43
45
  adapterOptions?: IAdapterOptions & {
44
46
  [key: string]: unknown;
45
47
  };
48
+ /**
49
+ * Virtual field definitions for context-aware query expansion.
50
+ * Virtual fields allow shortcuts like `my:assigned` that expand to
51
+ * real schema fields at query execution time.
52
+ *
53
+ * @example
54
+ * virtualFields: {
55
+ * my: {
56
+ * allowedValues: ['assigned', 'created'] as const,
57
+ * resolve: (input, ctx, { fields }) => ({
58
+ * type: 'comparison',
59
+ * field: fields({ assigned: 'assignee_id', created: 'creator_id' })[input.value],
60
+ * operator: '==',
61
+ * value: ctx.currentUserId
62
+ * })
63
+ * }
64
+ * }
65
+ */
66
+ virtualFields?: VirtualFieldsConfig<TSchema, TContext>;
67
+ /**
68
+ * Factory function to create query execution context.
69
+ * Called once per query execution to provide runtime values
70
+ * for virtual field resolution.
71
+ *
72
+ * @example
73
+ * createContext: async () => ({
74
+ * currentUserId: await getCurrentUserId(),
75
+ * currentUserTeamIds: await getUserTeamIds()
76
+ * })
77
+ */
78
+ createContext?: () => TContext | Promise<TContext>;
46
79
  }
47
80
  export interface IQueryExecutor<TResult> {
48
81
  execute(): Promise<TResult[]>;
@@ -66,10 +99,10 @@ export type QueryKit<TSchema extends Record<string, object>, TRows extends {
66
99
  /**
67
100
  * Create a new QueryKit instance
68
101
  */
69
- export declare function createQueryKit<TSchema extends Record<string, object>, TRows extends {
102
+ export declare function createQueryKit<TSchema extends Record<string, object>, TContext extends IQueryContext = IQueryContext, TRows extends {
70
103
  [K in keyof TSchema & string]: unknown;
71
104
  } = {
72
105
  [K in keyof TSchema & string]: unknown;
73
- }>(options: IQueryKitOptions<TSchema>): QueryKit<TSchema, TRows>;
106
+ }>(options: IQueryKitOptions<TSchema, TContext>): QueryKit<TSchema, TRows>;
74
107
  export * from './parser';
75
108
  export * from './security';
package/dist/index.js CHANGED
@@ -33,9 +33,11 @@ Object.defineProperty(exports, "QueryParser", { enumerable: true, get: function
33
33
  const sql_1 = require("./translators/sql");
34
34
  Object.defineProperty(exports, "SqlTranslator", { enumerable: true, get: function () { return sql_1.SqlTranslator; } });
35
35
  const security_1 = require("./security");
36
+ const virtual_fields_1 = require("./virtual-fields");
36
37
  // Re-export from modules
37
38
  __exportStar(require("./translators"), exports);
38
39
  __exportStar(require("./adapters"), exports);
40
+ __exportStar(require("./virtual-fields"), exports);
39
41
  /**
40
42
  * Create a new QueryBuilder instance
41
43
  */
@@ -75,9 +77,8 @@ function createQueryKit(options) {
75
77
  query: (table) => {
76
78
  return {
77
79
  where: (queryString) => {
78
- // Parse and validate the query
80
+ // Parse the query
79
81
  const expressionAst = parser.parse(queryString);
80
- securityValidator.validate(expressionAst, options.schema);
81
82
  // Execution state accumulated via fluent calls
82
83
  let orderByState = {};
83
84
  let limitState;
@@ -96,8 +97,24 @@ function createQueryKit(options) {
96
97
  return executor;
97
98
  },
98
99
  execute: async () => {
100
+ // Validate that if virtual fields are configured, createContext must also be provided
101
+ if (options.virtualFields && !options.createContext) {
102
+ throw new Error('createContext must be provided when virtualFields is configured');
103
+ }
104
+ // Get context if virtual fields are configured
105
+ let context;
106
+ if (options.virtualFields && options.createContext) {
107
+ context = await options.createContext();
108
+ }
109
+ // Resolve virtual fields if configured and context is available
110
+ let resolvedExpression = expressionAst;
111
+ if (options.virtualFields && context) {
112
+ resolvedExpression = (0, virtual_fields_1.resolveVirtualFields)(expressionAst, options.virtualFields, context);
113
+ }
114
+ // Validate the resolved query
115
+ securityValidator.validate(resolvedExpression, options.schema);
99
116
  // Delegate to adapter
100
- const results = await options.adapter.execute(table, expressionAst, {
117
+ const results = await options.adapter.execute(table, resolvedExpression, {
101
118
  orderBy: Object.keys(orderByState).length > 0
102
119
  ? orderByState
103
120
  : undefined,
@@ -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;