@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,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL Translator for QueryKit
|
|
3
|
+
*
|
|
4
|
+
* This translator converts QueryKit AST expressions into generic SQL
|
|
5
|
+
* WHERE clause conditions that can be used in any SQL query.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { IComparisonExpression, ILogicalExpression, QueryExpression } from '../../parser/types';
|
|
9
|
+
import { ITranslator, ITranslatorOptions } from '../types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Options specific to the SQL translator
|
|
13
|
+
*/
|
|
14
|
+
export interface ISqlTranslatorOptions extends ITranslatorOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Quote character for identifiers (field names)
|
|
17
|
+
* Default is double quotes (ANSI SQL standard)
|
|
18
|
+
*/
|
|
19
|
+
identifierQuote?: string;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Quote character for string literals
|
|
23
|
+
* Default is single quotes (ANSI SQL standard)
|
|
24
|
+
*/
|
|
25
|
+
stringLiteralQuote?: string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Whether to use parameters instead of inline values
|
|
29
|
+
* If true, translate() will return an object with sql and params
|
|
30
|
+
* Default is true for security reasons (protection against SQL injection)
|
|
31
|
+
*
|
|
32
|
+
* @warning Setting this to false may expose your application to SQL injection attacks.
|
|
33
|
+
* Only disable this if you have a very specific reason and you're handling the security
|
|
34
|
+
* implications yourself.
|
|
35
|
+
*/
|
|
36
|
+
useParameters?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The result of SQL translation
|
|
41
|
+
*/
|
|
42
|
+
export interface ISqlTranslationResult {
|
|
43
|
+
/**
|
|
44
|
+
* The SQL query string with placeholders for parameters
|
|
45
|
+
*/
|
|
46
|
+
sql: string;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The parameters to be used with the query
|
|
50
|
+
*/
|
|
51
|
+
params: unknown[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Error thrown when translation fails
|
|
56
|
+
*/
|
|
57
|
+
export class SqlTranslationError extends Error {
|
|
58
|
+
constructor(message: string) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.name = 'SqlTranslationError';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Translates QueryKit AST expressions to SQL WHERE clauses
|
|
66
|
+
*/
|
|
67
|
+
export class SqlTranslator implements ITranslator<ISqlTranslationResult | string> {
|
|
68
|
+
private options: Required<ISqlTranslatorOptions>;
|
|
69
|
+
private params: unknown[] = [];
|
|
70
|
+
|
|
71
|
+
constructor(options: ISqlTranslatorOptions = {}) {
|
|
72
|
+
this.options = {
|
|
73
|
+
normalizeFieldNames: options.normalizeFieldNames ?? false,
|
|
74
|
+
fieldMappings: options.fieldMappings ?? {},
|
|
75
|
+
identifierQuote: options.identifierQuote ?? '"',
|
|
76
|
+
stringLiteralQuote: options.stringLiteralQuote ?? "'",
|
|
77
|
+
useParameters: options.useParameters ?? true
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Translate a QueryKit expression to an SQL WHERE clause
|
|
83
|
+
*/
|
|
84
|
+
public translate(expression: QueryExpression): ISqlTranslationResult | string {
|
|
85
|
+
this.params = [];
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const sql = this.translateExpression(expression);
|
|
89
|
+
|
|
90
|
+
return this.options.useParameters
|
|
91
|
+
? { sql, params: [...this.params] }
|
|
92
|
+
: sql;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
throw new SqlTranslationError(
|
|
95
|
+
`Failed to translate expression: ${error instanceof Error ? error.message : String(error)}`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if an expression can be translated to SQL
|
|
102
|
+
*/
|
|
103
|
+
public canTranslate(expression: QueryExpression): boolean {
|
|
104
|
+
try {
|
|
105
|
+
this.translateExpression(expression);
|
|
106
|
+
return true;
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Translate any QueryKit expression to SQL
|
|
114
|
+
*/
|
|
115
|
+
private translateExpression(expression: QueryExpression): string {
|
|
116
|
+
if (!expression) {
|
|
117
|
+
throw new SqlTranslationError('Empty expression');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
switch (expression.type) {
|
|
121
|
+
case 'comparison':
|
|
122
|
+
return this.translateComparisonExpression(expression);
|
|
123
|
+
case 'logical':
|
|
124
|
+
return this.translateLogicalExpression(expression);
|
|
125
|
+
default:
|
|
126
|
+
throw new SqlTranslationError(`Unsupported expression type: ${(expression as { type: string }).type}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Translate a comparison expression to SQL
|
|
132
|
+
*/
|
|
133
|
+
private translateComparisonExpression(expression: IComparisonExpression): string {
|
|
134
|
+
const { field, operator, value } = expression;
|
|
135
|
+
const fieldName = this.normalizeField(field);
|
|
136
|
+
const quotedField = this.quoteIdentifier(fieldName);
|
|
137
|
+
|
|
138
|
+
// Handle each operator type
|
|
139
|
+
switch (operator) {
|
|
140
|
+
case '==':
|
|
141
|
+
return this.formatComparison(quotedField, '=', value);
|
|
142
|
+
case '!=':
|
|
143
|
+
return this.formatComparison(quotedField, '<>', value);
|
|
144
|
+
case '>':
|
|
145
|
+
return this.formatComparison(quotedField, '>', value);
|
|
146
|
+
case '>=':
|
|
147
|
+
return this.formatComparison(quotedField, '>=', value);
|
|
148
|
+
case '<':
|
|
149
|
+
return this.formatComparison(quotedField, '<', value);
|
|
150
|
+
case '<=':
|
|
151
|
+
return this.formatComparison(quotedField, '<=', value);
|
|
152
|
+
case 'LIKE': {
|
|
153
|
+
if (typeof value !== 'string') {
|
|
154
|
+
throw new SqlTranslationError('LIKE operator requires a string value');
|
|
155
|
+
}
|
|
156
|
+
// Convert wildcard syntax to SQL LIKE pattern
|
|
157
|
+
const sqlPattern = this.wildcardToSqlPattern(value);
|
|
158
|
+
return this.formatComparison(quotedField, 'LIKE', sqlPattern);
|
|
159
|
+
}
|
|
160
|
+
case 'IN': {
|
|
161
|
+
if (!Array.isArray(value)) {
|
|
162
|
+
throw new SqlTranslationError('IN operator requires an array value');
|
|
163
|
+
}
|
|
164
|
+
if (value.length === 0) {
|
|
165
|
+
// Empty IN clause should always be false
|
|
166
|
+
return 'FALSE';
|
|
167
|
+
}
|
|
168
|
+
return this.formatInClause(quotedField, value, false);
|
|
169
|
+
}
|
|
170
|
+
case 'NOT IN': {
|
|
171
|
+
if (!Array.isArray(value)) {
|
|
172
|
+
throw new SqlTranslationError('NOT IN operator requires an array value');
|
|
173
|
+
}
|
|
174
|
+
if (value.length === 0) {
|
|
175
|
+
// Empty NOT IN clause should always be true
|
|
176
|
+
return 'TRUE';
|
|
177
|
+
}
|
|
178
|
+
return this.formatInClause(quotedField, value, true);
|
|
179
|
+
}
|
|
180
|
+
default:
|
|
181
|
+
throw new SqlTranslationError(`Unsupported operator: ${operator}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Convert wildcard pattern to SQL LIKE pattern
|
|
187
|
+
*/
|
|
188
|
+
private wildcardToSqlPattern(pattern: string): string {
|
|
189
|
+
// Replace * with % and ? with _ for SQL LIKE syntax
|
|
190
|
+
// Also escape any existing SQL LIKE special characters
|
|
191
|
+
return pattern
|
|
192
|
+
.replace(/%/g, '\\%') // Escape existing %
|
|
193
|
+
.replace(/_/g, '\\_') // Escape existing _
|
|
194
|
+
.replace(/\*/g, '%') // * → %
|
|
195
|
+
.replace(/\?/g, '_'); // ? → _
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Translate a logical expression to SQL
|
|
200
|
+
*/
|
|
201
|
+
private translateLogicalExpression(expression: ILogicalExpression): string {
|
|
202
|
+
const { operator, left, right } = expression;
|
|
203
|
+
|
|
204
|
+
const leftSql = this.translateExpression(left);
|
|
205
|
+
|
|
206
|
+
if (operator === 'NOT') {
|
|
207
|
+
return `NOT (${leftSql})`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!right) {
|
|
211
|
+
throw new SqlTranslationError(`${operator} operator requires two operands`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const rightSql = this.translateExpression(right);
|
|
215
|
+
|
|
216
|
+
switch (operator) {
|
|
217
|
+
case 'AND':
|
|
218
|
+
return `(${leftSql}) AND (${rightSql})`;
|
|
219
|
+
case 'OR':
|
|
220
|
+
return `(${leftSql}) OR (${rightSql})`;
|
|
221
|
+
default:
|
|
222
|
+
throw new SqlTranslationError(`Unsupported logical operator: ${operator}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Format a comparison expression
|
|
228
|
+
*/
|
|
229
|
+
private formatComparison(field: string, sqlOperator: string, value: unknown): string {
|
|
230
|
+
if (value === null) {
|
|
231
|
+
// Handle NULL comparisons
|
|
232
|
+
return sqlOperator === '='
|
|
233
|
+
? `${field} IS NULL`
|
|
234
|
+
: `${field} IS NOT NULL`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const formattedValue = this.formatValue(value);
|
|
238
|
+
return `${field} ${sqlOperator} ${formattedValue}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Format an IN clause
|
|
243
|
+
*/
|
|
244
|
+
private formatInClause(field: string, values: unknown[], isNot: boolean): string {
|
|
245
|
+
const operator = isNot ? 'NOT IN' : 'IN';
|
|
246
|
+
|
|
247
|
+
if (this.options.useParameters) {
|
|
248
|
+
const placeholders = values.map(() => `?`);
|
|
249
|
+
this.params.push(...values);
|
|
250
|
+
return `${field} ${operator} (${placeholders.join(', ')})`;
|
|
251
|
+
} else {
|
|
252
|
+
const formattedValues = values.map(v => this.formatValue(v, false));
|
|
253
|
+
return `${field} ${operator} (${formattedValues.join(', ')})`;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Format a value for SQL
|
|
259
|
+
*/
|
|
260
|
+
private formatValue(value: unknown, addToParams = true): string {
|
|
261
|
+
if (this.options.useParameters && addToParams) {
|
|
262
|
+
this.params.push(value);
|
|
263
|
+
return '?';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (value === null) {
|
|
267
|
+
return 'NULL';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (typeof value === 'string') {
|
|
271
|
+
return `${this.options.stringLiteralQuote}${this.escapeString(value)}${this.options.stringLiteralQuote}`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
275
|
+
return String(value);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (value instanceof Date) {
|
|
279
|
+
return `${this.options.stringLiteralQuote}${value.toISOString()}${this.options.stringLiteralQuote}`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
283
|
+
throw new SqlTranslationError(`Complex objects are not supported as values. Got: ${Object.prototype.toString.call(value)}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (Array.isArray(value)) {
|
|
287
|
+
const formattedValues = value.map(v => this.formatValue(v, false));
|
|
288
|
+
return formattedValues.join(', ');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
throw new SqlTranslationError(`Unsupported value type: ${typeof value}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Escape a string literal for SQL
|
|
296
|
+
*/
|
|
297
|
+
private escapeString(str: string): string {
|
|
298
|
+
// Escape any quotes in the string by doubling them
|
|
299
|
+
return str.replace(
|
|
300
|
+
new RegExp(this.options.stringLiteralQuote, 'g'),
|
|
301
|
+
`${this.options.stringLiteralQuote}${this.options.stringLiteralQuote}`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Quote an identifier (field name) for SQL
|
|
307
|
+
*/
|
|
308
|
+
private quoteIdentifier(identifier: string): string {
|
|
309
|
+
// Handle table.column format
|
|
310
|
+
if (identifier.includes('.')) {
|
|
311
|
+
const parts = identifier.split('.');
|
|
312
|
+
return parts.map(part => this.quoteIdentifier(part)).join('.');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const quote = this.options.identifierQuote;
|
|
316
|
+
// Escape any quotes in the identifier by doubling them
|
|
317
|
+
const escaped = identifier.replace(new RegExp(quote, 'g'), `${quote}${quote}`);
|
|
318
|
+
return `${quote}${escaped}${quote}`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Normalize a field name based on translator options
|
|
323
|
+
*/
|
|
324
|
+
private normalizeField(field: string): string {
|
|
325
|
+
const normalizedField = this.options.normalizeFieldNames
|
|
326
|
+
? field.toLowerCase()
|
|
327
|
+
: field;
|
|
328
|
+
|
|
329
|
+
return this.options.fieldMappings[normalizedField] ?? normalizedField;
|
|
330
|
+
}
|
|
331
|
+
}
|