@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.
Files changed (118) hide show
  1. package/.cursor/BUGBOT.md +21 -0
  2. package/.cursor/rules/01-project-structure.mdc +77 -0
  3. package/.cursor/rules/02-typescript-standards.mdc +105 -0
  4. package/.cursor/rules/03-testing-standards.mdc +78 -0
  5. package/.cursor/rules/04-query-language.mdc +79 -0
  6. package/.cursor/rules/05-solid-principles.mdc +118 -0
  7. package/.cursor/rules/liqe-readme-docs.mdc +438 -0
  8. package/.devcontainer/devcontainer.json +25 -0
  9. package/.eslintignore +1 -0
  10. package/.eslintrc.js +39 -0
  11. package/.github/dependabot.yml +12 -0
  12. package/.github/workflows/ci.yml +114 -0
  13. package/.github/workflows/publish.yml +61 -0
  14. package/.husky/pre-commit +30 -0
  15. package/.prettierrc +10 -0
  16. package/CONTRIBUTING.md +187 -0
  17. package/LICENSE +674 -0
  18. package/README.md +237 -0
  19. package/dist/adapters/drizzle/index.d.ts +122 -0
  20. package/dist/adapters/drizzle/index.js +166 -0
  21. package/dist/adapters/index.d.ts +7 -0
  22. package/dist/adapters/index.js +25 -0
  23. package/dist/adapters/types.d.ts +60 -0
  24. package/dist/adapters/types.js +8 -0
  25. package/dist/index.d.ts +75 -0
  26. package/dist/index.js +118 -0
  27. package/dist/parser/index.d.ts +2 -0
  28. package/dist/parser/index.js +18 -0
  29. package/dist/parser/parser.d.ts +51 -0
  30. package/dist/parser/parser.js +201 -0
  31. package/dist/parser/types.d.ts +68 -0
  32. package/dist/parser/types.js +5 -0
  33. package/dist/query/builder.d.ts +61 -0
  34. package/dist/query/builder.js +188 -0
  35. package/dist/query/index.d.ts +2 -0
  36. package/dist/query/index.js +18 -0
  37. package/dist/query/types.d.ts +79 -0
  38. package/dist/query/types.js +2 -0
  39. package/dist/security/index.d.ts +2 -0
  40. package/dist/security/index.js +18 -0
  41. package/dist/security/types.d.ts +181 -0
  42. package/dist/security/types.js +43 -0
  43. package/dist/security/validator.d.ts +191 -0
  44. package/dist/security/validator.js +344 -0
  45. package/dist/translators/drizzle/index.d.ts +73 -0
  46. package/dist/translators/drizzle/index.js +260 -0
  47. package/dist/translators/index.d.ts +8 -0
  48. package/dist/translators/index.js +27 -0
  49. package/dist/translators/sql/index.d.ts +108 -0
  50. package/dist/translators/sql/index.js +252 -0
  51. package/dist/translators/types.d.ts +39 -0
  52. package/dist/translators/types.js +8 -0
  53. package/examples/qk-next/README.md +35 -0
  54. package/examples/qk-next/app/favicon.ico +0 -0
  55. package/examples/qk-next/app/globals.css +122 -0
  56. package/examples/qk-next/app/layout.tsx +121 -0
  57. package/examples/qk-next/app/page.tsx +813 -0
  58. package/examples/qk-next/app/providers.tsx +80 -0
  59. package/examples/qk-next/components/aurora-background.tsx +12 -0
  60. package/examples/qk-next/components/github-stars.tsx +51 -0
  61. package/examples/qk-next/components/mode-toggle.tsx +27 -0
  62. package/examples/qk-next/components/reactbits/blocks/Backgrounds/Aurora/Aurora.tsx +217 -0
  63. package/examples/qk-next/components/reactbits/blocks/Backgrounds/LightRays/LightRays.tsx +474 -0
  64. package/examples/qk-next/components/theme-provider.tsx +11 -0
  65. package/examples/qk-next/components/ui/card.tsx +92 -0
  66. package/examples/qk-next/components/ui/command.tsx +184 -0
  67. package/examples/qk-next/components/ui/dialog.tsx +143 -0
  68. package/examples/qk-next/components/ui/drawer.tsx +135 -0
  69. package/examples/qk-next/components/ui/hover-card.tsx +44 -0
  70. package/examples/qk-next/components/ui/icons.tsx +148 -0
  71. package/examples/qk-next/components/ui/sonner.tsx +26 -0
  72. package/examples/qk-next/components/ui/table.tsx +117 -0
  73. package/examples/qk-next/components.json +21 -0
  74. package/examples/qk-next/eslint.config.mjs +21 -0
  75. package/examples/qk-next/jsrepo.json +13 -0
  76. package/examples/qk-next/lib/utils.ts +6 -0
  77. package/examples/qk-next/next.config.ts +8 -0
  78. package/examples/qk-next/package.json +48 -0
  79. package/examples/qk-next/pnpm-lock.yaml +5558 -0
  80. package/examples/qk-next/postcss.config.mjs +5 -0
  81. package/examples/qk-next/public/file.svg +1 -0
  82. package/examples/qk-next/public/globe.svg +1 -0
  83. package/examples/qk-next/public/next.svg +1 -0
  84. package/examples/qk-next/public/vercel.svg +1 -0
  85. package/examples/qk-next/public/window.svg +1 -0
  86. package/examples/qk-next/tsconfig.json +42 -0
  87. package/examples/qk-next/types/sonner.d.ts +3 -0
  88. package/jest.config.js +26 -0
  89. package/package.json +51 -0
  90. package/src/adapters/drizzle/drizzle-adapter.test.ts +115 -0
  91. package/src/adapters/drizzle/index.ts +299 -0
  92. package/src/adapters/index.ts +11 -0
  93. package/src/adapters/types.ts +72 -0
  94. package/src/index.ts +194 -0
  95. package/src/integration.test.ts +202 -0
  96. package/src/parser/index.ts +2 -0
  97. package/src/parser/parser.test.ts +1056 -0
  98. package/src/parser/parser.ts +268 -0
  99. package/src/parser/types.ts +97 -0
  100. package/src/query/builder.test.ts +272 -0
  101. package/src/query/builder.ts +274 -0
  102. package/src/query/index.ts +2 -0
  103. package/src/query/types.ts +107 -0
  104. package/src/security/index.ts +2 -0
  105. package/src/security/types.ts +210 -0
  106. package/src/security/validator.test.ts +459 -0
  107. package/src/security/validator.ts +395 -0
  108. package/src/security.test.ts +366 -0
  109. package/src/translators/drizzle/drizzle-translator.test.ts +128 -0
  110. package/src/translators/drizzle/index.test.ts +45 -0
  111. package/src/translators/drizzle/index.ts +346 -0
  112. package/src/translators/index.ts +14 -0
  113. package/src/translators/sql/index.test.ts +45 -0
  114. package/src/translators/sql/index.ts +331 -0
  115. package/src/translators/sql/sql-translator.test.ts +419 -0
  116. package/src/translators/types.ts +44 -0
  117. package/src/types/sonner.d.ts +3 -0
  118. 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,8 @@
1
+ /**
2
+ * QueryKit Translators
3
+ *
4
+ * Exports all translator implementations for different target platforms.
5
+ */
6
+ export * from './types';
7
+ export * from './drizzle';
8
+ export * from './sql';
@@ -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
+ }