@gblikas/querykit 0.0.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 +21 -0
- package/.cursor/rules/01-project-structure.mdc +77 -0
- package/.cursor/rules/02-typescript-standards.mdc +105 -0
- package/.cursor/rules/03-testing-standards.mdc +78 -0
- package/.cursor/rules/04-query-language.mdc +79 -0
- package/.cursor/rules/05-solid-principles.mdc +118 -0
- package/.cursor/rules/liqe-readme-docs.mdc +438 -0
- package/.devcontainer/devcontainer.json +25 -0
- package/.eslintignore +1 -0
- package/.eslintrc.js +39 -0
- package/.github/dependabot.yml +12 -0
- package/.github/workflows/ci.yml +114 -0
- package/.github/workflows/publish.yml +61 -0
- package/.husky/pre-commit +30 -0
- package/.prettierrc +10 -0
- package/CONTRIBUTING.md +187 -0
- package/LICENSE +674 -0
- package/README.md +237 -0
- package/dist/adapters/drizzle/index.d.ts +122 -0
- package/dist/adapters/drizzle/index.js +166 -0
- package/dist/adapters/index.d.ts +7 -0
- package/dist/adapters/index.js +25 -0
- package/dist/adapters/types.d.ts +60 -0
- package/dist/adapters/types.js +8 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.js +118 -0
- package/dist/parser/index.d.ts +2 -0
- package/dist/parser/index.js +18 -0
- package/dist/parser/parser.d.ts +51 -0
- package/dist/parser/parser.js +201 -0
- package/dist/parser/types.d.ts +68 -0
- package/dist/parser/types.js +5 -0
- package/dist/query/builder.d.ts +61 -0
- package/dist/query/builder.js +188 -0
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.js +18 -0
- package/dist/query/types.d.ts +79 -0
- package/dist/query/types.js +2 -0
- package/dist/security/index.d.ts +2 -0
- package/dist/security/index.js +18 -0
- package/dist/security/types.d.ts +181 -0
- package/dist/security/types.js +43 -0
- package/dist/security/validator.d.ts +191 -0
- package/dist/security/validator.js +344 -0
- package/dist/translators/drizzle/index.d.ts +73 -0
- package/dist/translators/drizzle/index.js +260 -0
- package/dist/translators/index.d.ts +8 -0
- package/dist/translators/index.js +27 -0
- package/dist/translators/sql/index.d.ts +108 -0
- package/dist/translators/sql/index.js +252 -0
- package/dist/translators/types.d.ts +39 -0
- package/dist/translators/types.js +8 -0
- package/examples/qk-next/README.md +35 -0
- package/examples/qk-next/app/favicon.ico +0 -0
- package/examples/qk-next/app/globals.css +122 -0
- package/examples/qk-next/app/layout.tsx +121 -0
- package/examples/qk-next/app/page.tsx +813 -0
- package/examples/qk-next/app/providers.tsx +80 -0
- package/examples/qk-next/components/aurora-background.tsx +12 -0
- package/examples/qk-next/components/github-stars.tsx +51 -0
- package/examples/qk-next/components/mode-toggle.tsx +27 -0
- package/examples/qk-next/components/reactbits/blocks/Backgrounds/Aurora/Aurora.tsx +217 -0
- package/examples/qk-next/components/reactbits/blocks/Backgrounds/LightRays/LightRays.tsx +474 -0
- package/examples/qk-next/components/theme-provider.tsx +11 -0
- package/examples/qk-next/components/ui/card.tsx +92 -0
- package/examples/qk-next/components/ui/command.tsx +184 -0
- package/examples/qk-next/components/ui/dialog.tsx +143 -0
- package/examples/qk-next/components/ui/drawer.tsx +135 -0
- package/examples/qk-next/components/ui/hover-card.tsx +44 -0
- package/examples/qk-next/components/ui/icons.tsx +148 -0
- package/examples/qk-next/components/ui/sonner.tsx +26 -0
- package/examples/qk-next/components/ui/table.tsx +117 -0
- package/examples/qk-next/components.json +21 -0
- package/examples/qk-next/eslint.config.mjs +21 -0
- package/examples/qk-next/jsrepo.json +13 -0
- package/examples/qk-next/lib/utils.ts +6 -0
- package/examples/qk-next/next.config.ts +8 -0
- package/examples/qk-next/package.json +48 -0
- package/examples/qk-next/pnpm-lock.yaml +5558 -0
- package/examples/qk-next/postcss.config.mjs +5 -0
- package/examples/qk-next/public/file.svg +1 -0
- package/examples/qk-next/public/globe.svg +1 -0
- package/examples/qk-next/public/next.svg +1 -0
- package/examples/qk-next/public/vercel.svg +1 -0
- package/examples/qk-next/public/window.svg +1 -0
- package/examples/qk-next/tsconfig.json +42 -0
- package/examples/qk-next/types/sonner.d.ts +3 -0
- package/jest.config.js +26 -0
- package/package.json +51 -0
- package/src/adapters/drizzle/drizzle-adapter.test.ts +115 -0
- package/src/adapters/drizzle/index.ts +299 -0
- package/src/adapters/index.ts +11 -0
- package/src/adapters/types.ts +72 -0
- package/src/index.ts +194 -0
- package/src/integration.test.ts +202 -0
- package/src/parser/index.ts +2 -0
- package/src/parser/parser.test.ts +1056 -0
- package/src/parser/parser.ts +268 -0
- package/src/parser/types.ts +97 -0
- package/src/query/builder.test.ts +272 -0
- package/src/query/builder.ts +274 -0
- package/src/query/index.ts +2 -0
- package/src/query/types.ts +107 -0
- package/src/security/index.ts +2 -0
- package/src/security/types.ts +210 -0
- package/src/security/validator.test.ts +459 -0
- package/src/security/validator.ts +395 -0
- package/src/security.test.ts +366 -0
- package/src/translators/drizzle/drizzle-translator.test.ts +128 -0
- package/src/translators/drizzle/index.test.ts +45 -0
- package/src/translators/drizzle/index.ts +346 -0
- package/src/translators/index.ts +14 -0
- package/src/translators/sql/index.test.ts +45 -0
- package/src/translators/sql/index.ts +331 -0
- package/src/translators/sql/sql-translator.test.ts +419 -0
- package/src/translators/types.ts +44 -0
- package/src/types/sonner.d.ts +3 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { parse as liqeParse } from 'liqe';
|
|
2
|
+
import type {
|
|
3
|
+
BooleanOperatorToken,
|
|
4
|
+
ExpressionToken,
|
|
5
|
+
FieldToken,
|
|
6
|
+
ImplicitBooleanOperatorToken,
|
|
7
|
+
LiqeQuery,
|
|
8
|
+
LogicalExpressionToken,
|
|
9
|
+
ParenthesizedExpressionToken,
|
|
10
|
+
TagToken,
|
|
11
|
+
UnaryOperatorToken
|
|
12
|
+
} from 'liqe';
|
|
13
|
+
import {
|
|
14
|
+
ComparisonOperator,
|
|
15
|
+
IComparisonExpression,
|
|
16
|
+
ILogicalExpression,
|
|
17
|
+
IParserOptions,
|
|
18
|
+
IQueryParser,
|
|
19
|
+
QueryExpression,
|
|
20
|
+
QueryValue
|
|
21
|
+
} from './types';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Error thrown when query parsing fails
|
|
25
|
+
*/
|
|
26
|
+
export class QueryParseError extends Error {
|
|
27
|
+
constructor(message: string) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = 'QueryParseError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Implementation of the QueryKit parser using Liqe
|
|
35
|
+
*/
|
|
36
|
+
export class QueryParser implements IQueryParser {
|
|
37
|
+
private options: Required<IParserOptions>;
|
|
38
|
+
|
|
39
|
+
constructor(options: IParserOptions = {}) {
|
|
40
|
+
this.options = {
|
|
41
|
+
caseInsensitiveFields: options.caseInsensitiveFields ?? false,
|
|
42
|
+
fieldMappings: options.fieldMappings ?? {}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Parse a query string into a QueryKit AST
|
|
48
|
+
*/
|
|
49
|
+
public parse(query: string): QueryExpression {
|
|
50
|
+
try {
|
|
51
|
+
const liqeAst = liqeParse(query);
|
|
52
|
+
return this.convertLiqeAst(liqeAst);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
throw new QueryParseError(
|
|
55
|
+
`Failed to parse query: ${error instanceof Error ? error.message : String(error)}`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Validate a query string
|
|
62
|
+
*/
|
|
63
|
+
public validate(query: string): boolean {
|
|
64
|
+
try {
|
|
65
|
+
const ast = liqeParse(query);
|
|
66
|
+
this.convertLiqeAst(ast);
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Convert a Liqe AST node to a QueryKit expression
|
|
75
|
+
*/
|
|
76
|
+
private convertLiqeAst(node: LiqeQuery): QueryExpression {
|
|
77
|
+
if (!node || typeof node !== 'object') {
|
|
78
|
+
throw new QueryParseError('Invalid AST node');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
switch (node.type) {
|
|
82
|
+
case 'LogicalExpression': {
|
|
83
|
+
const logicalNode = node as LogicalExpressionToken;
|
|
84
|
+
const operator = (logicalNode.operator as BooleanOperatorToken | ImplicitBooleanOperatorToken).operator;
|
|
85
|
+
return this.createLogicalExpression(
|
|
86
|
+
this.convertLogicalOperator(operator),
|
|
87
|
+
logicalNode.left,
|
|
88
|
+
logicalNode.right
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
case 'UnaryOperator': {
|
|
93
|
+
const unaryNode = node as UnaryOperatorToken;
|
|
94
|
+
return this.createLogicalExpression('NOT', unaryNode.operand);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
case 'Tag': {
|
|
98
|
+
const tagNode = node as TagToken;
|
|
99
|
+
const field = tagNode.field as FieldToken;
|
|
100
|
+
const expression = tagNode.expression as ExpressionToken & { value: QueryValue };
|
|
101
|
+
|
|
102
|
+
if (!field || !expression) {
|
|
103
|
+
throw new QueryParseError('Invalid field or expression in Tag node');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const fieldName = this.normalizeFieldName(field.name);
|
|
107
|
+
const operator = this.convertLiqeOperator(tagNode.operator.operator);
|
|
108
|
+
const value = this.convertLiqeValue(expression.value);
|
|
109
|
+
|
|
110
|
+
// Check for wildcard patterns in string values
|
|
111
|
+
if (operator === '==' && typeof value === 'string' && (value.includes('*') || value.includes('?'))) {
|
|
112
|
+
return this.createComparisonExpression(
|
|
113
|
+
fieldName,
|
|
114
|
+
'LIKE',
|
|
115
|
+
value
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return this.createComparisonExpression(
|
|
120
|
+
fieldName,
|
|
121
|
+
operator,
|
|
122
|
+
value
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
case 'EmptyExpression':
|
|
127
|
+
if ('left' in node && node.left) {
|
|
128
|
+
return this.convertLiqeAst(node.left);
|
|
129
|
+
}
|
|
130
|
+
throw new QueryParseError('Invalid empty expression');
|
|
131
|
+
|
|
132
|
+
case 'ParenthesizedExpression': {
|
|
133
|
+
const parenNode = node as ParenthesizedExpressionToken;
|
|
134
|
+
if (parenNode.expression) {
|
|
135
|
+
return this.convertLiqeAst(parenNode.expression);
|
|
136
|
+
}
|
|
137
|
+
throw new QueryParseError('Invalid parenthesized expression');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
default:
|
|
141
|
+
throw new QueryParseError(`Unsupported node type: ${(node as { type: string }).type}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Convert a Liqe logical operator to a QueryKit operator
|
|
147
|
+
*/
|
|
148
|
+
private convertLogicalOperator(operator: string): 'AND' | 'OR' | 'NOT' {
|
|
149
|
+
switch (operator.toLowerCase()) {
|
|
150
|
+
case 'and':
|
|
151
|
+
return 'AND';
|
|
152
|
+
case 'or':
|
|
153
|
+
return 'OR';
|
|
154
|
+
case 'not':
|
|
155
|
+
return 'NOT';
|
|
156
|
+
default:
|
|
157
|
+
throw new QueryParseError(`Unsupported logical operator: ${operator}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Create a logical expression from Liqe nodes
|
|
163
|
+
*/
|
|
164
|
+
private createLogicalExpression(
|
|
165
|
+
operator: 'AND' | 'OR' | 'NOT',
|
|
166
|
+
left: LiqeQuery,
|
|
167
|
+
right?: LiqeQuery
|
|
168
|
+
): ILogicalExpression {
|
|
169
|
+
return {
|
|
170
|
+
type: 'logical',
|
|
171
|
+
operator,
|
|
172
|
+
left: this.convertLiqeAst(left),
|
|
173
|
+
...(right && { right: this.convertLiqeAst(right) })
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Create a comparison expression
|
|
179
|
+
*/
|
|
180
|
+
private createComparisonExpression(
|
|
181
|
+
field: string,
|
|
182
|
+
operator: ComparisonOperator,
|
|
183
|
+
value: QueryValue
|
|
184
|
+
): IComparisonExpression {
|
|
185
|
+
return {
|
|
186
|
+
type: 'comparison',
|
|
187
|
+
field,
|
|
188
|
+
operator,
|
|
189
|
+
value
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Convert a Liqe operator to a QueryKit operator
|
|
195
|
+
*/
|
|
196
|
+
private convertLiqeOperator(operator: string): ComparisonOperator {
|
|
197
|
+
// Handle the case where operator is part of the value for comparison operators
|
|
198
|
+
if (operator === ':') {
|
|
199
|
+
return '==';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Check if the operator is prefixed with a colon
|
|
203
|
+
const actualOperator = operator.startsWith(':') ? operator.substring(1) : operator;
|
|
204
|
+
|
|
205
|
+
// Map Liqe operators to QueryKit operators
|
|
206
|
+
const operatorMap: Record<string, ComparisonOperator> = {
|
|
207
|
+
'=': '==',
|
|
208
|
+
'!=': '!=',
|
|
209
|
+
'>': '>',
|
|
210
|
+
'>=': '>=',
|
|
211
|
+
'<': '<',
|
|
212
|
+
'<=': '<=',
|
|
213
|
+
'in': 'IN',
|
|
214
|
+
'not in': 'NOT IN'
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const queryKitOperator = operatorMap[actualOperator.toLowerCase()];
|
|
218
|
+
if (!queryKitOperator) {
|
|
219
|
+
throw new QueryParseError(`Unsupported operator: ${operator}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return queryKitOperator;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Convert a Liqe value to a QueryKit value
|
|
227
|
+
* Security: Strict type checking to prevent NoSQL injection via objects
|
|
228
|
+
*/
|
|
229
|
+
private convertLiqeValue(value: unknown): QueryValue {
|
|
230
|
+
// Security fix: Strict type checking to prevent object injection
|
|
231
|
+
if (value === null) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
236
|
+
return value as QueryValue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (Array.isArray(value)) {
|
|
240
|
+
// Security fix: Recursively validate array elements
|
|
241
|
+
const validatedArray = value.map(item => {
|
|
242
|
+
if (typeof item === 'object' && item !== null) {
|
|
243
|
+
throw new QueryParseError('Object values are not allowed in arrays');
|
|
244
|
+
}
|
|
245
|
+
return this.convertLiqeValue(item);
|
|
246
|
+
});
|
|
247
|
+
return validatedArray as QueryValue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Security fix: Reject all object types to prevent NoSQL injection
|
|
251
|
+
if (typeof value === 'object') {
|
|
252
|
+
throw new QueryParseError('Object values are not supported for security reasons');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
throw new QueryParseError(`Unsupported value type: ${typeof value}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Normalize a field name based on parser options
|
|
260
|
+
*/
|
|
261
|
+
private normalizeFieldName(field: string): string {
|
|
262
|
+
const normalizedField = this.options.caseInsensitiveFields
|
|
263
|
+
? field.toLowerCase()
|
|
264
|
+
: field;
|
|
265
|
+
|
|
266
|
+
return this.options.fieldMappings[normalizedField] ?? normalizedField;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core AST types for QueryKit's parser
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Represents a comparison operator in a query expression
|
|
7
|
+
*/
|
|
8
|
+
export type ComparisonOperator =
|
|
9
|
+
| '=='
|
|
10
|
+
| '!='
|
|
11
|
+
| '>'
|
|
12
|
+
| '>='
|
|
13
|
+
| '<'
|
|
14
|
+
| '<='
|
|
15
|
+
| 'IN'
|
|
16
|
+
| 'NOT IN'
|
|
17
|
+
| 'LIKE';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Represents a logical operator in a query expression
|
|
21
|
+
*/
|
|
22
|
+
export type LogicalOperator =
|
|
23
|
+
| 'AND'
|
|
24
|
+
| 'OR'
|
|
25
|
+
| 'NOT';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Represents a value that can be used in a query expression
|
|
29
|
+
*/
|
|
30
|
+
export type QueryValue =
|
|
31
|
+
| string
|
|
32
|
+
| number
|
|
33
|
+
| boolean
|
|
34
|
+
| null
|
|
35
|
+
| Array<string | number | boolean | null>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Represents a comparison expression node in the AST
|
|
39
|
+
*/
|
|
40
|
+
export interface IComparisonExpression {
|
|
41
|
+
type: 'comparison';
|
|
42
|
+
field: string;
|
|
43
|
+
operator: ComparisonOperator;
|
|
44
|
+
value: QueryValue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Represents a logical expression node in the AST
|
|
49
|
+
*/
|
|
50
|
+
export interface ILogicalExpression {
|
|
51
|
+
type: 'logical';
|
|
52
|
+
operator: LogicalOperator;
|
|
53
|
+
left: QueryExpression;
|
|
54
|
+
right?: QueryExpression;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Represents any valid query expression node
|
|
59
|
+
*/
|
|
60
|
+
export type QueryExpression =
|
|
61
|
+
| IComparisonExpression
|
|
62
|
+
| ILogicalExpression;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Configuration options for the parser
|
|
66
|
+
*/
|
|
67
|
+
export interface IParserOptions {
|
|
68
|
+
/**
|
|
69
|
+
* Whether to allow case-insensitive field names
|
|
70
|
+
*/
|
|
71
|
+
caseInsensitiveFields?: boolean;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Custom field name mappings
|
|
75
|
+
*/
|
|
76
|
+
fieldMappings?: Record<string, string>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Interface for the query parser
|
|
81
|
+
*/
|
|
82
|
+
export interface IQueryParser {
|
|
83
|
+
/**
|
|
84
|
+
* Parse a query string into an AST
|
|
85
|
+
* @param query The query string to parse
|
|
86
|
+
* @returns The parsed AST
|
|
87
|
+
* @throws {QueryParseError} If the query is invalid
|
|
88
|
+
*/
|
|
89
|
+
parse(query: string): QueryExpression;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Validate a query string without fully parsing it
|
|
93
|
+
* @param query The query string to validate
|
|
94
|
+
* @returns true if the query is valid, false otherwise
|
|
95
|
+
*/
|
|
96
|
+
validate(query: string): boolean;
|
|
97
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { QueryBuilder } from './builder';
|
|
2
|
+
import { ComparisonOperator } from './types';
|
|
3
|
+
|
|
4
|
+
interface ITodo {
|
|
5
|
+
id: number;
|
|
6
|
+
title: string;
|
|
7
|
+
priority: number;
|
|
8
|
+
status: string;
|
|
9
|
+
dueDate: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('QueryBuilder', () => {
|
|
13
|
+
let builder: QueryBuilder<ITodo>;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
builder = new QueryBuilder<ITodo>();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('where', () => {
|
|
20
|
+
it('should create a simple comparison query', () => {
|
|
21
|
+
const query = builder.where('priority', '>', 2).toString();
|
|
22
|
+
|
|
23
|
+
expect(query).toBe('priority:>2');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should handle string values', () => {
|
|
27
|
+
const query = builder.where('status', '==', 'active').toString();
|
|
28
|
+
|
|
29
|
+
expect(query).toBe('status:"active"');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should handle null values', () => {
|
|
33
|
+
const query = builder.where('dueDate', '==', null).toString();
|
|
34
|
+
|
|
35
|
+
expect(query).toBe('dueDate:null');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should handle array values', () => {
|
|
39
|
+
const query = builder
|
|
40
|
+
.where('status', 'IN', ['active', 'pending'])
|
|
41
|
+
.toString();
|
|
42
|
+
|
|
43
|
+
expect(query).toBe('status:in["active","pending"]');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should accept direct query string syntax', () => {
|
|
47
|
+
const query = builder.where('priority:>2').toString();
|
|
48
|
+
|
|
49
|
+
expect(query).toBe('priority:>2');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should combine direct query strings with AND', () => {
|
|
53
|
+
const query = builder
|
|
54
|
+
.where('priority:>2')
|
|
55
|
+
.andWhere('status:"active"')
|
|
56
|
+
.toString();
|
|
57
|
+
|
|
58
|
+
expect(query).toBe('(priority:>2) AND status:"active"');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('andWhere', () => {
|
|
63
|
+
it('should combine conditions with AND', () => {
|
|
64
|
+
const query = builder
|
|
65
|
+
.where('priority', '>', 2)
|
|
66
|
+
.andWhere('status', '==', 'active')
|
|
67
|
+
.toString();
|
|
68
|
+
|
|
69
|
+
expect(query).toBe('(priority:>2) AND status:"active"');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should handle multiple AND conditions', () => {
|
|
73
|
+
const query = builder
|
|
74
|
+
.where('priority', '>', 2)
|
|
75
|
+
.andWhere('status', '==', 'active')
|
|
76
|
+
.andWhere('dueDate', '!=', null)
|
|
77
|
+
.toString();
|
|
78
|
+
|
|
79
|
+
expect(query).toBe(
|
|
80
|
+
'((priority:>2) AND status:"active") AND dueDate:!=null'
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should handle andWhere as first condition with field-operator-value', () => {
|
|
85
|
+
const query = builder.andWhere('priority', '>', 2).toString();
|
|
86
|
+
|
|
87
|
+
expect(query).toBe('priority:>2');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should handle andWhere as first condition with query string', () => {
|
|
91
|
+
const query = builder.andWhere('priority:>2').toString();
|
|
92
|
+
|
|
93
|
+
expect(query).toBe('priority:>2');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('orWhere', () => {
|
|
98
|
+
it('should combine conditions with OR', () => {
|
|
99
|
+
const query = builder
|
|
100
|
+
.where('status', '==', 'active')
|
|
101
|
+
.orWhere('status', '==', 'pending')
|
|
102
|
+
.toString();
|
|
103
|
+
|
|
104
|
+
expect(query).toBe('(status:"active") OR status:"pending"');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should handle multiple OR conditions', () => {
|
|
108
|
+
const query = builder
|
|
109
|
+
.where('status', '==', 'active')
|
|
110
|
+
.orWhere('status', '==', 'pending')
|
|
111
|
+
.orWhere('status', '==', 'inactive')
|
|
112
|
+
.toString();
|
|
113
|
+
|
|
114
|
+
expect(query).toBe(
|
|
115
|
+
'((status:"active") OR status:"pending") OR status:"inactive"'
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should handle orWhere as first condition with field-operator-value', () => {
|
|
120
|
+
const query = builder.orWhere('priority', '>', 2).toString();
|
|
121
|
+
|
|
122
|
+
expect(query).toBe('priority:>2');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should handle orWhere as first condition with query string', () => {
|
|
126
|
+
const query = builder.orWhere('priority:>2').toString();
|
|
127
|
+
|
|
128
|
+
expect(query).toBe('priority:>2');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('notWhere', () => {
|
|
133
|
+
it('should create a NOT condition', () => {
|
|
134
|
+
const query = builder.notWhere('status', '==', 'inactive').toString();
|
|
135
|
+
|
|
136
|
+
expect(query).toBe('NOT status:"inactive"');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should combine NOT with other conditions', () => {
|
|
140
|
+
const query = builder
|
|
141
|
+
.where('priority', '>', 2)
|
|
142
|
+
.notWhere('status', '==', 'inactive')
|
|
143
|
+
.toString();
|
|
144
|
+
|
|
145
|
+
expect(query).toBe('(priority:>2) AND NOT status:"inactive"');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should handle notWhere as first condition with query string', () => {
|
|
149
|
+
const query = builder.notWhere('status:"inactive"').toString();
|
|
150
|
+
|
|
151
|
+
expect(query).toBe('NOT status:"inactive"');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should handle notWhere with query string and existing expression', () => {
|
|
155
|
+
const query = builder
|
|
156
|
+
.where('priority', '>', 2)
|
|
157
|
+
.notWhere('status:"inactive"')
|
|
158
|
+
.toString();
|
|
159
|
+
|
|
160
|
+
expect(query).toBe('(priority:>2) AND NOT status:"inactive"');
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('orderBy', () => {
|
|
165
|
+
it('should add an ORDER BY clause', () => {
|
|
166
|
+
const query = builder
|
|
167
|
+
.where('priority', '>', 2)
|
|
168
|
+
.orderBy('title', 'asc')
|
|
169
|
+
.toString();
|
|
170
|
+
|
|
171
|
+
expect(query).toBe('priority:>2 ORDER BY title ASC');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should use ASC as default direction', () => {
|
|
175
|
+
const query = builder
|
|
176
|
+
.where('priority', '>', 2)
|
|
177
|
+
.orderBy('title')
|
|
178
|
+
.toString();
|
|
179
|
+
|
|
180
|
+
expect(query).toBe('priority:>2 ORDER BY title ASC');
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('limit and offset', () => {
|
|
185
|
+
it('should add LIMIT and OFFSET clauses', () => {
|
|
186
|
+
const query = builder
|
|
187
|
+
.where('priority', '>', 2)
|
|
188
|
+
.limit(10)
|
|
189
|
+
.offset(20)
|
|
190
|
+
.toString();
|
|
191
|
+
|
|
192
|
+
expect(query).toBe('priority:>2 LIMIT 10 OFFSET 20');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('getExpression', () => {
|
|
197
|
+
it('should return a valid expression', () => {
|
|
198
|
+
const expression = builder
|
|
199
|
+
.where('priority', '>', 2)
|
|
200
|
+
.andWhere('status', '==', 'active')
|
|
201
|
+
.getExpression();
|
|
202
|
+
|
|
203
|
+
expect(expression).toEqual({
|
|
204
|
+
type: 'logical',
|
|
205
|
+
operator: 'AND',
|
|
206
|
+
left: {
|
|
207
|
+
type: 'comparison',
|
|
208
|
+
field: 'priority',
|
|
209
|
+
operator: '>',
|
|
210
|
+
value: 2
|
|
211
|
+
},
|
|
212
|
+
right: {
|
|
213
|
+
type: 'comparison',
|
|
214
|
+
field: 'status',
|
|
215
|
+
operator: '==',
|
|
216
|
+
value: 'active'
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('wildcard support', () => {
|
|
223
|
+
it('should support wildcard syntax in direct query strings', () => {
|
|
224
|
+
const query = builder.where('title:Task*').toString();
|
|
225
|
+
expect(query).toBe('title:Task*'); // Preserves the wildcard in the query
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should support wildcard syntax in field-operator-value API', () => {
|
|
229
|
+
const query = builder
|
|
230
|
+
.where('title', 'LIKE' as ComparisonOperator, 'Task*')
|
|
231
|
+
.toString();
|
|
232
|
+
expect(query).toBe('title:Task*'); // Should use the colon format for LIKE
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should support combined wildcards', () => {
|
|
236
|
+
const query = builder.where('title:*Important*').toString();
|
|
237
|
+
expect(query).toBe('title:*Important*');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should support ? wildcards', () => {
|
|
241
|
+
const query = builder.where('code:ABC?').toString();
|
|
242
|
+
expect(query).toBe('code:ABC?');
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('custom operator handling', () => {
|
|
247
|
+
it('should handle NOT IN operator properly', () => {
|
|
248
|
+
const query = builder
|
|
249
|
+
.where('status', 'NOT IN' as ComparisonOperator, [
|
|
250
|
+
'inactive',
|
|
251
|
+
'deleted'
|
|
252
|
+
])
|
|
253
|
+
.toString();
|
|
254
|
+
|
|
255
|
+
expect(query).toBe('status:not in["inactive","deleted"]');
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe('constructor options', () => {
|
|
260
|
+
it('should initialize with custom options', () => {
|
|
261
|
+
const options = {
|
|
262
|
+
caseInsensitiveFields: true,
|
|
263
|
+
fieldMappings: { title: 'task_title' }
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const customBuilder = new QueryBuilder<ITodo>(options);
|
|
267
|
+
const query = customBuilder.where('title', '==', 'Bug fix').toString();
|
|
268
|
+
|
|
269
|
+
expect(query).toBe('title:"Bug fix"');
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
});
|