@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,260 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Drizzle ORM Translator for QueryKit
|
|
4
|
+
*
|
|
5
|
+
* This translator converts QueryKit AST expressions into Drizzle ORM
|
|
6
|
+
* query conditions that can be used in Drizzle's SQL query builder.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.DrizzleTranslator = exports.DrizzleTranslationError = void 0;
|
|
10
|
+
const drizzle_orm_1 = require("drizzle-orm");
|
|
11
|
+
/**
|
|
12
|
+
* Error thrown when translation fails
|
|
13
|
+
*/
|
|
14
|
+
class DrizzleTranslationError extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'DrizzleTranslationError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
exports.DrizzleTranslationError = DrizzleTranslationError;
|
|
21
|
+
/**
|
|
22
|
+
* Translates QueryKit AST expressions to Drizzle ORM conditions
|
|
23
|
+
*/
|
|
24
|
+
class DrizzleTranslator {
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
this.options = {
|
|
27
|
+
normalizeFieldNames: options.normalizeFieldNames ?? false,
|
|
28
|
+
fieldMappings: options.fieldMappings ?? {},
|
|
29
|
+
schema: options.schema ?? {}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Translate a QueryKit expression to a Drizzle ORM condition
|
|
34
|
+
*/
|
|
35
|
+
translate(expression) {
|
|
36
|
+
try {
|
|
37
|
+
return this.translateExpression(expression);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw new DrizzleTranslationError(`Failed to translate expression: ${error instanceof Error ? error.message : String(error)}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Check if an expression can be translated to Drizzle ORM
|
|
45
|
+
*/
|
|
46
|
+
canTranslate(expression) {
|
|
47
|
+
try {
|
|
48
|
+
this.translateExpression(expression);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Translate any QueryKit expression to a Drizzle ORM condition
|
|
57
|
+
*/
|
|
58
|
+
translateExpression(expression) {
|
|
59
|
+
if (!expression) {
|
|
60
|
+
throw new DrizzleTranslationError('Empty expression');
|
|
61
|
+
}
|
|
62
|
+
switch (expression.type) {
|
|
63
|
+
case 'comparison':
|
|
64
|
+
return this.translateComparisonExpression(expression);
|
|
65
|
+
case 'logical':
|
|
66
|
+
return this.translateLogicalExpression(expression);
|
|
67
|
+
default:
|
|
68
|
+
throw new DrizzleTranslationError(`Unsupported expression type: ${expression.type}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Translate a comparison expression to a Drizzle ORM condition
|
|
73
|
+
*/
|
|
74
|
+
translateComparisonExpression(expression) {
|
|
75
|
+
const { field, operator, value } = expression;
|
|
76
|
+
const fieldName = this.normalizeField(field);
|
|
77
|
+
// Get the field from the schema if available
|
|
78
|
+
const schemaField = this.getSchemaField(fieldName);
|
|
79
|
+
// If we have a schema field, use it directly with Drizzle operators
|
|
80
|
+
if (schemaField) {
|
|
81
|
+
switch (operator) {
|
|
82
|
+
case '==':
|
|
83
|
+
return (0, drizzle_orm_1.eq)(schemaField, value);
|
|
84
|
+
case '!=':
|
|
85
|
+
return (0, drizzle_orm_1.ne)(schemaField, value);
|
|
86
|
+
case '>':
|
|
87
|
+
return (0, drizzle_orm_1.gt)(schemaField, value);
|
|
88
|
+
case '>=':
|
|
89
|
+
return (0, drizzle_orm_1.gte)(schemaField, value);
|
|
90
|
+
case '<':
|
|
91
|
+
return (0, drizzle_orm_1.lt)(schemaField, value);
|
|
92
|
+
case '<=':
|
|
93
|
+
return (0, drizzle_orm_1.lte)(schemaField, value);
|
|
94
|
+
case 'LIKE': {
|
|
95
|
+
if (typeof value !== 'string') {
|
|
96
|
+
throw new DrizzleTranslationError('LIKE operator requires a string value');
|
|
97
|
+
}
|
|
98
|
+
// Convert wildcard to SQL pattern and use the like function
|
|
99
|
+
const sqlPattern = this.wildcardToSqlPattern(value);
|
|
100
|
+
return (0, drizzle_orm_1.sql) `${schemaField} LIKE ${sqlPattern}`;
|
|
101
|
+
}
|
|
102
|
+
case 'IN':
|
|
103
|
+
if (!Array.isArray(value)) {
|
|
104
|
+
throw new DrizzleTranslationError('IN operator requires an array value');
|
|
105
|
+
}
|
|
106
|
+
return (0, drizzle_orm_1.inArray)(schemaField, value);
|
|
107
|
+
case 'NOT IN':
|
|
108
|
+
if (!Array.isArray(value)) {
|
|
109
|
+
throw new DrizzleTranslationError('NOT IN operator requires an array value');
|
|
110
|
+
}
|
|
111
|
+
return (0, drizzle_orm_1.notInArray)(schemaField, value);
|
|
112
|
+
default:
|
|
113
|
+
throw new DrizzleTranslationError(`Unsupported operator: ${operator}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// If we don't have a schema field, we need to build the SQL manually
|
|
117
|
+
// Handle each operator type
|
|
118
|
+
return this.buildSqlForOperator(fieldName, operator, value);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Build SQL for a specific operator with raw field name
|
|
122
|
+
* Security: Validates field names to prevent SQL injection
|
|
123
|
+
*/
|
|
124
|
+
buildSqlForOperator(fieldName, operator, value) {
|
|
125
|
+
// Security fix: Validate field name format before using it (prevents SQL injection)
|
|
126
|
+
if (!this.isValidFieldName(fieldName)) {
|
|
127
|
+
throw new DrizzleTranslationError(`Invalid field name: ${fieldName}`);
|
|
128
|
+
}
|
|
129
|
+
switch (operator) {
|
|
130
|
+
case '==':
|
|
131
|
+
return (0, drizzle_orm_1.sql) `${drizzle_orm_1.sql.identifier(fieldName)} = ${value}`;
|
|
132
|
+
case '!=':
|
|
133
|
+
return (0, drizzle_orm_1.sql) `${drizzle_orm_1.sql.identifier(fieldName)} != ${value}`;
|
|
134
|
+
case '>':
|
|
135
|
+
return (0, drizzle_orm_1.sql) `${drizzle_orm_1.sql.identifier(fieldName)} > ${value}`;
|
|
136
|
+
case '>=':
|
|
137
|
+
return (0, drizzle_orm_1.sql) `${drizzle_orm_1.sql.identifier(fieldName)} >= ${value}`;
|
|
138
|
+
case '<':
|
|
139
|
+
return (0, drizzle_orm_1.sql) `${drizzle_orm_1.sql.identifier(fieldName)} < ${value}`;
|
|
140
|
+
case '<=':
|
|
141
|
+
return (0, drizzle_orm_1.sql) `${drizzle_orm_1.sql.identifier(fieldName)} <= ${value}`;
|
|
142
|
+
case 'LIKE': {
|
|
143
|
+
if (typeof value !== 'string') {
|
|
144
|
+
throw new DrizzleTranslationError('LIKE operator requires a string value');
|
|
145
|
+
}
|
|
146
|
+
// Convert wildcard to SQL pattern
|
|
147
|
+
const sqlPattern = this.wildcardToSqlPattern(value);
|
|
148
|
+
return (0, drizzle_orm_1.sql) `${drizzle_orm_1.sql.identifier(fieldName)} LIKE ${sqlPattern}`;
|
|
149
|
+
}
|
|
150
|
+
case 'IN': {
|
|
151
|
+
if (!Array.isArray(value)) {
|
|
152
|
+
throw new DrizzleTranslationError('IN operator requires an array value');
|
|
153
|
+
}
|
|
154
|
+
if (value.length === 0) {
|
|
155
|
+
// Empty IN clause should always be false
|
|
156
|
+
return (0, drizzle_orm_1.sql) `FALSE`;
|
|
157
|
+
}
|
|
158
|
+
return (0, drizzle_orm_1.sql) `${drizzle_orm_1.sql.identifier(fieldName)} IN (${value})`;
|
|
159
|
+
}
|
|
160
|
+
case 'NOT IN': {
|
|
161
|
+
if (!Array.isArray(value)) {
|
|
162
|
+
throw new DrizzleTranslationError('NOT IN operator requires an array value');
|
|
163
|
+
}
|
|
164
|
+
if (value.length === 0) {
|
|
165
|
+
// Empty NOT IN clause should always be true
|
|
166
|
+
return (0, drizzle_orm_1.sql) `TRUE`;
|
|
167
|
+
}
|
|
168
|
+
return (0, drizzle_orm_1.sql) `${drizzle_orm_1.sql.identifier(fieldName)} NOT IN (${value})`;
|
|
169
|
+
}
|
|
170
|
+
default:
|
|
171
|
+
throw new DrizzleTranslationError(`Unsupported operator: ${operator}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Convert wildcard pattern to SQL LIKE pattern
|
|
176
|
+
*/
|
|
177
|
+
wildcardToSqlPattern(pattern) {
|
|
178
|
+
// Replace * with % and ? with _ for SQL LIKE syntax
|
|
179
|
+
// Also escape any existing SQL LIKE special characters
|
|
180
|
+
return pattern
|
|
181
|
+
.replace(/%/g, '\\%') // Escape existing %
|
|
182
|
+
.replace(/_/g, '\\_') // Escape existing _
|
|
183
|
+
.replace(/\*/g, '%') // * → %
|
|
184
|
+
.replace(/\?/g, '_'); // ? → _
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Translate a logical expression to a Drizzle ORM condition
|
|
188
|
+
*/
|
|
189
|
+
translateLogicalExpression(expression) {
|
|
190
|
+
const { operator, left, right } = expression;
|
|
191
|
+
const leftSql = this.translateExpression(left);
|
|
192
|
+
if (operator === 'NOT') {
|
|
193
|
+
return (0, drizzle_orm_1.sql) `NOT (${leftSql})`;
|
|
194
|
+
}
|
|
195
|
+
if (!right) {
|
|
196
|
+
throw new DrizzleTranslationError(`${operator} operator requires two operands`);
|
|
197
|
+
}
|
|
198
|
+
const rightSql = this.translateExpression(right);
|
|
199
|
+
switch (operator) {
|
|
200
|
+
case 'AND':
|
|
201
|
+
return (0, drizzle_orm_1.sql) `(${leftSql}) AND (${rightSql})`;
|
|
202
|
+
case 'OR':
|
|
203
|
+
return (0, drizzle_orm_1.sql) `(${leftSql}) OR (${rightSql})`;
|
|
204
|
+
default:
|
|
205
|
+
throw new DrizzleTranslationError(`Unsupported logical operator: ${operator}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Normalize a field name based on translator options
|
|
210
|
+
*/
|
|
211
|
+
normalizeField(field) {
|
|
212
|
+
const normalizedField = this.options.normalizeFieldNames
|
|
213
|
+
? field.toLowerCase()
|
|
214
|
+
: field;
|
|
215
|
+
return this.options.fieldMappings[normalizedField] ?? normalizedField;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Get a field from the schema if it exists
|
|
219
|
+
*/
|
|
220
|
+
getSchemaField(fieldName) {
|
|
221
|
+
// Extract table and column names from fieldName (e.g., 'users.id' -> { table: 'users', column: 'id' })
|
|
222
|
+
const parts = fieldName.split('.');
|
|
223
|
+
if (parts.length === 2) {
|
|
224
|
+
const [tableName, columnName] = parts;
|
|
225
|
+
const table = this.options.schema[tableName];
|
|
226
|
+
if (table && columnName in table) {
|
|
227
|
+
return table[columnName];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Heuristic: if there's only one table in the schema, allow bare field lookup
|
|
231
|
+
if (parts.length === 1) {
|
|
232
|
+
const [onlyTableName] = Object.keys(this.options.schema);
|
|
233
|
+
if (onlyTableName) {
|
|
234
|
+
const table = this.options.schema[onlyTableName];
|
|
235
|
+
const columnName = parts[0];
|
|
236
|
+
if (table && columnName in table) {
|
|
237
|
+
return table[columnName];
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// If the field is not found in the schema
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Security: Validates field names to prevent SQL injection
|
|
246
|
+
* Only allows alphanumeric chars, dots, underscores. Max 64 chars per part.
|
|
247
|
+
*/
|
|
248
|
+
isValidFieldName(fieldName) {
|
|
249
|
+
const validFieldPattern = /^[a-zA-Z][a-zA-Z0-9._]*$/;
|
|
250
|
+
const parts = fieldName.split('.');
|
|
251
|
+
// Only allow table.column format (max 2 parts)
|
|
252
|
+
if (parts.length > 2)
|
|
253
|
+
return false;
|
|
254
|
+
return parts.every(part => validFieldPattern.test(part) &&
|
|
255
|
+
part.length <= 64 &&
|
|
256
|
+
!part.includes('__') // Prevent reserved patterns
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
exports.DrizzleTranslator = DrizzleTranslator;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* QueryKit Translators
|
|
4
|
+
*
|
|
5
|
+
* Exports all translator implementations for different target platforms.
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
19
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
20
|
+
};
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
// Export base types
|
|
23
|
+
__exportStar(require("./types"), exports);
|
|
24
|
+
// Export Drizzle translator
|
|
25
|
+
__exportStar(require("./drizzle"), exports);
|
|
26
|
+
// Export SQL translator
|
|
27
|
+
__exportStar(require("./sql"), exports);
|
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
import { QueryExpression } from '../../parser/types';
|
|
8
|
+
import { ITranslator, ITranslatorOptions } from '../types';
|
|
9
|
+
/**
|
|
10
|
+
* Options specific to the SQL translator
|
|
11
|
+
*/
|
|
12
|
+
export interface ISqlTranslatorOptions extends ITranslatorOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Quote character for identifiers (field names)
|
|
15
|
+
* Default is double quotes (ANSI SQL standard)
|
|
16
|
+
*/
|
|
17
|
+
identifierQuote?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Quote character for string literals
|
|
20
|
+
* Default is single quotes (ANSI SQL standard)
|
|
21
|
+
*/
|
|
22
|
+
stringLiteralQuote?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Whether to use parameters instead of inline values
|
|
25
|
+
* If true, translate() will return an object with sql and params
|
|
26
|
+
* Default is true for security reasons (protection against SQL injection)
|
|
27
|
+
*
|
|
28
|
+
* @warning Setting this to false may expose your application to SQL injection attacks.
|
|
29
|
+
* Only disable this if you have a very specific reason and you're handling the security
|
|
30
|
+
* implications yourself.
|
|
31
|
+
*/
|
|
32
|
+
useParameters?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The result of SQL translation
|
|
36
|
+
*/
|
|
37
|
+
export interface ISqlTranslationResult {
|
|
38
|
+
/**
|
|
39
|
+
* The SQL query string with placeholders for parameters
|
|
40
|
+
*/
|
|
41
|
+
sql: string;
|
|
42
|
+
/**
|
|
43
|
+
* The parameters to be used with the query
|
|
44
|
+
*/
|
|
45
|
+
params: unknown[];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Error thrown when translation fails
|
|
49
|
+
*/
|
|
50
|
+
export declare class SqlTranslationError extends Error {
|
|
51
|
+
constructor(message: string);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Translates QueryKit AST expressions to SQL WHERE clauses
|
|
55
|
+
*/
|
|
56
|
+
export declare class SqlTranslator implements ITranslator<ISqlTranslationResult | string> {
|
|
57
|
+
private options;
|
|
58
|
+
private params;
|
|
59
|
+
constructor(options?: ISqlTranslatorOptions);
|
|
60
|
+
/**
|
|
61
|
+
* Translate a QueryKit expression to an SQL WHERE clause
|
|
62
|
+
*/
|
|
63
|
+
translate(expression: QueryExpression): ISqlTranslationResult | string;
|
|
64
|
+
/**
|
|
65
|
+
* Check if an expression can be translated to SQL
|
|
66
|
+
*/
|
|
67
|
+
canTranslate(expression: QueryExpression): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Translate any QueryKit expression to SQL
|
|
70
|
+
*/
|
|
71
|
+
private translateExpression;
|
|
72
|
+
/**
|
|
73
|
+
* Translate a comparison expression to SQL
|
|
74
|
+
*/
|
|
75
|
+
private translateComparisonExpression;
|
|
76
|
+
/**
|
|
77
|
+
* Convert wildcard pattern to SQL LIKE pattern
|
|
78
|
+
*/
|
|
79
|
+
private wildcardToSqlPattern;
|
|
80
|
+
/**
|
|
81
|
+
* Translate a logical expression to SQL
|
|
82
|
+
*/
|
|
83
|
+
private translateLogicalExpression;
|
|
84
|
+
/**
|
|
85
|
+
* Format a comparison expression
|
|
86
|
+
*/
|
|
87
|
+
private formatComparison;
|
|
88
|
+
/**
|
|
89
|
+
* Format an IN clause
|
|
90
|
+
*/
|
|
91
|
+
private formatInClause;
|
|
92
|
+
/**
|
|
93
|
+
* Format a value for SQL
|
|
94
|
+
*/
|
|
95
|
+
private formatValue;
|
|
96
|
+
/**
|
|
97
|
+
* Escape a string literal for SQL
|
|
98
|
+
*/
|
|
99
|
+
private escapeString;
|
|
100
|
+
/**
|
|
101
|
+
* Quote an identifier (field name) for SQL
|
|
102
|
+
*/
|
|
103
|
+
private quoteIdentifier;
|
|
104
|
+
/**
|
|
105
|
+
* Normalize a field name based on translator options
|
|
106
|
+
*/
|
|
107
|
+
private normalizeField;
|
|
108
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SQL Translator for QueryKit
|
|
4
|
+
*
|
|
5
|
+
* This translator converts QueryKit AST expressions into generic SQL
|
|
6
|
+
* WHERE clause conditions that can be used in any SQL query.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.SqlTranslator = exports.SqlTranslationError = void 0;
|
|
10
|
+
/**
|
|
11
|
+
* Error thrown when translation fails
|
|
12
|
+
*/
|
|
13
|
+
class SqlTranslationError extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'SqlTranslationError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.SqlTranslationError = SqlTranslationError;
|
|
20
|
+
/**
|
|
21
|
+
* Translates QueryKit AST expressions to SQL WHERE clauses
|
|
22
|
+
*/
|
|
23
|
+
class SqlTranslator {
|
|
24
|
+
constructor(options = {}) {
|
|
25
|
+
this.params = [];
|
|
26
|
+
this.options = {
|
|
27
|
+
normalizeFieldNames: options.normalizeFieldNames ?? false,
|
|
28
|
+
fieldMappings: options.fieldMappings ?? {},
|
|
29
|
+
identifierQuote: options.identifierQuote ?? '"',
|
|
30
|
+
stringLiteralQuote: options.stringLiteralQuote ?? "'",
|
|
31
|
+
useParameters: options.useParameters ?? true
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Translate a QueryKit expression to an SQL WHERE clause
|
|
36
|
+
*/
|
|
37
|
+
translate(expression) {
|
|
38
|
+
this.params = [];
|
|
39
|
+
try {
|
|
40
|
+
const sql = this.translateExpression(expression);
|
|
41
|
+
return this.options.useParameters
|
|
42
|
+
? { sql, params: [...this.params] }
|
|
43
|
+
: sql;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
throw new SqlTranslationError(`Failed to translate expression: ${error instanceof Error ? error.message : String(error)}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Check if an expression can be translated to SQL
|
|
51
|
+
*/
|
|
52
|
+
canTranslate(expression) {
|
|
53
|
+
try {
|
|
54
|
+
this.translateExpression(expression);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Translate any QueryKit expression to SQL
|
|
63
|
+
*/
|
|
64
|
+
translateExpression(expression) {
|
|
65
|
+
if (!expression) {
|
|
66
|
+
throw new SqlTranslationError('Empty expression');
|
|
67
|
+
}
|
|
68
|
+
switch (expression.type) {
|
|
69
|
+
case 'comparison':
|
|
70
|
+
return this.translateComparisonExpression(expression);
|
|
71
|
+
case 'logical':
|
|
72
|
+
return this.translateLogicalExpression(expression);
|
|
73
|
+
default:
|
|
74
|
+
throw new SqlTranslationError(`Unsupported expression type: ${expression.type}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Translate a comparison expression to SQL
|
|
79
|
+
*/
|
|
80
|
+
translateComparisonExpression(expression) {
|
|
81
|
+
const { field, operator, value } = expression;
|
|
82
|
+
const fieldName = this.normalizeField(field);
|
|
83
|
+
const quotedField = this.quoteIdentifier(fieldName);
|
|
84
|
+
// Handle each operator type
|
|
85
|
+
switch (operator) {
|
|
86
|
+
case '==':
|
|
87
|
+
return this.formatComparison(quotedField, '=', value);
|
|
88
|
+
case '!=':
|
|
89
|
+
return this.formatComparison(quotedField, '<>', value);
|
|
90
|
+
case '>':
|
|
91
|
+
return this.formatComparison(quotedField, '>', value);
|
|
92
|
+
case '>=':
|
|
93
|
+
return this.formatComparison(quotedField, '>=', value);
|
|
94
|
+
case '<':
|
|
95
|
+
return this.formatComparison(quotedField, '<', value);
|
|
96
|
+
case '<=':
|
|
97
|
+
return this.formatComparison(quotedField, '<=', value);
|
|
98
|
+
case 'LIKE': {
|
|
99
|
+
if (typeof value !== 'string') {
|
|
100
|
+
throw new SqlTranslationError('LIKE operator requires a string value');
|
|
101
|
+
}
|
|
102
|
+
// Convert wildcard syntax to SQL LIKE pattern
|
|
103
|
+
const sqlPattern = this.wildcardToSqlPattern(value);
|
|
104
|
+
return this.formatComparison(quotedField, 'LIKE', sqlPattern);
|
|
105
|
+
}
|
|
106
|
+
case 'IN': {
|
|
107
|
+
if (!Array.isArray(value)) {
|
|
108
|
+
throw new SqlTranslationError('IN operator requires an array value');
|
|
109
|
+
}
|
|
110
|
+
if (value.length === 0) {
|
|
111
|
+
// Empty IN clause should always be false
|
|
112
|
+
return 'FALSE';
|
|
113
|
+
}
|
|
114
|
+
return this.formatInClause(quotedField, value, false);
|
|
115
|
+
}
|
|
116
|
+
case 'NOT IN': {
|
|
117
|
+
if (!Array.isArray(value)) {
|
|
118
|
+
throw new SqlTranslationError('NOT IN operator requires an array value');
|
|
119
|
+
}
|
|
120
|
+
if (value.length === 0) {
|
|
121
|
+
// Empty NOT IN clause should always be true
|
|
122
|
+
return 'TRUE';
|
|
123
|
+
}
|
|
124
|
+
return this.formatInClause(quotedField, value, true);
|
|
125
|
+
}
|
|
126
|
+
default:
|
|
127
|
+
throw new SqlTranslationError(`Unsupported operator: ${operator}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Convert wildcard pattern to SQL LIKE pattern
|
|
132
|
+
*/
|
|
133
|
+
wildcardToSqlPattern(pattern) {
|
|
134
|
+
// Replace * with % and ? with _ for SQL LIKE syntax
|
|
135
|
+
// Also escape any existing SQL LIKE special characters
|
|
136
|
+
return pattern
|
|
137
|
+
.replace(/%/g, '\\%') // Escape existing %
|
|
138
|
+
.replace(/_/g, '\\_') // Escape existing _
|
|
139
|
+
.replace(/\*/g, '%') // * → %
|
|
140
|
+
.replace(/\?/g, '_'); // ? → _
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Translate a logical expression to SQL
|
|
144
|
+
*/
|
|
145
|
+
translateLogicalExpression(expression) {
|
|
146
|
+
const { operator, left, right } = expression;
|
|
147
|
+
const leftSql = this.translateExpression(left);
|
|
148
|
+
if (operator === 'NOT') {
|
|
149
|
+
return `NOT (${leftSql})`;
|
|
150
|
+
}
|
|
151
|
+
if (!right) {
|
|
152
|
+
throw new SqlTranslationError(`${operator} operator requires two operands`);
|
|
153
|
+
}
|
|
154
|
+
const rightSql = this.translateExpression(right);
|
|
155
|
+
switch (operator) {
|
|
156
|
+
case 'AND':
|
|
157
|
+
return `(${leftSql}) AND (${rightSql})`;
|
|
158
|
+
case 'OR':
|
|
159
|
+
return `(${leftSql}) OR (${rightSql})`;
|
|
160
|
+
default:
|
|
161
|
+
throw new SqlTranslationError(`Unsupported logical operator: ${operator}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Format a comparison expression
|
|
166
|
+
*/
|
|
167
|
+
formatComparison(field, sqlOperator, value) {
|
|
168
|
+
if (value === null) {
|
|
169
|
+
// Handle NULL comparisons
|
|
170
|
+
return sqlOperator === '='
|
|
171
|
+
? `${field} IS NULL`
|
|
172
|
+
: `${field} IS NOT NULL`;
|
|
173
|
+
}
|
|
174
|
+
const formattedValue = this.formatValue(value);
|
|
175
|
+
return `${field} ${sqlOperator} ${formattedValue}`;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Format an IN clause
|
|
179
|
+
*/
|
|
180
|
+
formatInClause(field, values, isNot) {
|
|
181
|
+
const operator = isNot ? 'NOT IN' : 'IN';
|
|
182
|
+
if (this.options.useParameters) {
|
|
183
|
+
const placeholders = values.map(() => `?`);
|
|
184
|
+
this.params.push(...values);
|
|
185
|
+
return `${field} ${operator} (${placeholders.join(', ')})`;
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
const formattedValues = values.map(v => this.formatValue(v, false));
|
|
189
|
+
return `${field} ${operator} (${formattedValues.join(', ')})`;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Format a value for SQL
|
|
194
|
+
*/
|
|
195
|
+
formatValue(value, addToParams = true) {
|
|
196
|
+
if (this.options.useParameters && addToParams) {
|
|
197
|
+
this.params.push(value);
|
|
198
|
+
return '?';
|
|
199
|
+
}
|
|
200
|
+
if (value === null) {
|
|
201
|
+
return 'NULL';
|
|
202
|
+
}
|
|
203
|
+
if (typeof value === 'string') {
|
|
204
|
+
return `${this.options.stringLiteralQuote}${this.escapeString(value)}${this.options.stringLiteralQuote}`;
|
|
205
|
+
}
|
|
206
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
207
|
+
return String(value);
|
|
208
|
+
}
|
|
209
|
+
if (value instanceof Date) {
|
|
210
|
+
return `${this.options.stringLiteralQuote}${value.toISOString()}${this.options.stringLiteralQuote}`;
|
|
211
|
+
}
|
|
212
|
+
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
213
|
+
throw new SqlTranslationError(`Complex objects are not supported as values. Got: ${Object.prototype.toString.call(value)}`);
|
|
214
|
+
}
|
|
215
|
+
if (Array.isArray(value)) {
|
|
216
|
+
const formattedValues = value.map(v => this.formatValue(v, false));
|
|
217
|
+
return formattedValues.join(', ');
|
|
218
|
+
}
|
|
219
|
+
throw new SqlTranslationError(`Unsupported value type: ${typeof value}`);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Escape a string literal for SQL
|
|
223
|
+
*/
|
|
224
|
+
escapeString(str) {
|
|
225
|
+
// Escape any quotes in the string by doubling them
|
|
226
|
+
return str.replace(new RegExp(this.options.stringLiteralQuote, 'g'), `${this.options.stringLiteralQuote}${this.options.stringLiteralQuote}`);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Quote an identifier (field name) for SQL
|
|
230
|
+
*/
|
|
231
|
+
quoteIdentifier(identifier) {
|
|
232
|
+
// Handle table.column format
|
|
233
|
+
if (identifier.includes('.')) {
|
|
234
|
+
const parts = identifier.split('.');
|
|
235
|
+
return parts.map(part => this.quoteIdentifier(part)).join('.');
|
|
236
|
+
}
|
|
237
|
+
const quote = this.options.identifierQuote;
|
|
238
|
+
// Escape any quotes in the identifier by doubling them
|
|
239
|
+
const escaped = identifier.replace(new RegExp(quote, 'g'), `${quote}${quote}`);
|
|
240
|
+
return `${quote}${escaped}${quote}`;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Normalize a field name based on translator options
|
|
244
|
+
*/
|
|
245
|
+
normalizeField(field) {
|
|
246
|
+
const normalizedField = this.options.normalizeFieldNames
|
|
247
|
+
? field.toLowerCase()
|
|
248
|
+
: field;
|
|
249
|
+
return this.options.fieldMappings[normalizedField] ?? normalizedField;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
exports.SqlTranslator = SqlTranslator;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QueryKit Translator Types
|
|
3
|
+
*
|
|
4
|
+
* These are the core interfaces for translators, which convert QueryKit's
|
|
5
|
+
* internal AST representation into formats that specific data sources can understand.
|
|
6
|
+
*/
|
|
7
|
+
import { QueryExpression } from '../parser/types';
|
|
8
|
+
/**
|
|
9
|
+
* Options for configuring a translator
|
|
10
|
+
*/
|
|
11
|
+
export interface ITranslatorOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Whether to normalize field names (e.g., lowercase them)
|
|
14
|
+
*/
|
|
15
|
+
normalizeFieldNames?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Custom field mappings from QueryKit fields to target fields
|
|
18
|
+
*/
|
|
19
|
+
fieldMappings?: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Interface for a query translator
|
|
23
|
+
*/
|
|
24
|
+
export interface ITranslator<T = unknown> {
|
|
25
|
+
/**
|
|
26
|
+
* Translate a QueryKit expression into the target format
|
|
27
|
+
*
|
|
28
|
+
* @param expression The QueryKit expression to translate
|
|
29
|
+
* @returns The translated query in the target format
|
|
30
|
+
*/
|
|
31
|
+
translate(expression: QueryExpression): T;
|
|
32
|
+
/**
|
|
33
|
+
* Check if an expression can be translated
|
|
34
|
+
*
|
|
35
|
+
* @param expression The expression to check
|
|
36
|
+
* @returns true if the expression can be translated, false otherwise
|
|
37
|
+
*/
|
|
38
|
+
canTranslate(expression: QueryExpression): boolean;
|
|
39
|
+
}
|