@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.
- package/.cursor/BUGBOT.md +65 -2
- package/.husky/pre-commit +3 -3
- package/README.md +510 -1
- package/dist/index.d.ts +36 -3
- package/dist/index.js +20 -3
- package/dist/parser/index.d.ts +1 -0
- package/dist/parser/index.js +1 -0
- package/dist/parser/input-parser.d.ts +215 -0
- package/dist/parser/input-parser.js +493 -0
- package/dist/parser/parser.d.ts +114 -1
- package/dist/parser/parser.js +716 -0
- package/dist/parser/types.d.ts +432 -0
- package/dist/virtual-fields/index.d.ts +5 -0
- package/dist/virtual-fields/index.js +21 -0
- package/dist/virtual-fields/resolver.d.ts +17 -0
- package/dist/virtual-fields/resolver.js +107 -0
- package/dist/virtual-fields/types.d.ts +160 -0
- package/dist/virtual-fields/types.js +5 -0
- package/examples/qk-next/app/page.tsx +190 -86
- package/examples/qk-next/package.json +1 -1
- package/package.json +2 -2
- package/src/adapters/drizzle/index.ts +3 -3
- package/src/index.ts +77 -8
- package/src/parser/divergence.test.ts +357 -0
- package/src/parser/index.ts +2 -1
- package/src/parser/input-parser.test.ts +770 -0
- package/src/parser/input-parser.ts +697 -0
- package/src/parser/parse-with-context-suggestions.test.ts +360 -0
- package/src/parser/parse-with-context-validation.test.ts +447 -0
- package/src/parser/parse-with-context.test.ts +325 -0
- package/src/parser/parser.ts +872 -0
- package/src/parser/token-consistency.test.ts +341 -0
- package/src/parser/types.ts +545 -23
- package/src/virtual-fields/index.ts +6 -0
- package/src/virtual-fields/integration.test.ts +338 -0
- package/src/virtual-fields/resolver.ts +165 -0
- package/src/virtual-fields/types.ts +203 -0
- package/src/virtual-fields/virtual-fields.test.ts +831 -0
- 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
|
|
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,
|
|
117
|
+
const results = await options.adapter.execute(table, resolvedExpression, {
|
|
101
118
|
orderBy: Object.keys(orderByState).length > 0
|
|
102
119
|
? orderByState
|
|
103
120
|
: undefined,
|
package/dist/parser/index.d.ts
CHANGED
package/dist/parser/index.js
CHANGED
|
@@ -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;
|