@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,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
+ }