@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,346 @@
1
+ /**
2
+ * Drizzle ORM Translator for QueryKit
3
+ *
4
+ * This translator converts QueryKit AST expressions into Drizzle ORM
5
+ * query conditions that can be used in Drizzle's SQL query builder.
6
+ */
7
+
8
+ import {
9
+ SQL,
10
+ SQLWrapper,
11
+ eq,
12
+ gt,
13
+ gte,
14
+ inArray,
15
+ lt,
16
+ lte,
17
+ ne,
18
+ notInArray,
19
+ sql
20
+ } from 'drizzle-orm';
21
+ import {
22
+ IComparisonExpression,
23
+ ILogicalExpression,
24
+ QueryExpression
25
+ } from '../../parser/types';
26
+ import { ITranslator, ITranslatorOptions } from '../types';
27
+
28
+ /**
29
+ * Options specific to the Drizzle translator
30
+ */
31
+ export interface IDrizzleTranslatorOptions extends ITranslatorOptions {
32
+ /**
33
+ * Schema information for type safety
34
+ */
35
+ schema?: Record<string, Record<string, unknown>>;
36
+ }
37
+
38
+ /**
39
+ * Error thrown when translation fails
40
+ */
41
+ export class DrizzleTranslationError extends Error {
42
+ constructor(message: string) {
43
+ super(message);
44
+ this.name = 'DrizzleTranslationError';
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Translates QueryKit AST expressions to Drizzle ORM conditions
50
+ */
51
+ export class DrizzleTranslator implements ITranslator<SQL> {
52
+ private options: Required<IDrizzleTranslatorOptions>;
53
+
54
+ constructor(options: IDrizzleTranslatorOptions = {}) {
55
+ this.options = {
56
+ normalizeFieldNames: options.normalizeFieldNames ?? false,
57
+ fieldMappings: options.fieldMappings ?? {},
58
+ schema: options.schema ?? {}
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Translate a QueryKit expression to a Drizzle ORM condition
64
+ */
65
+ public translate(expression: QueryExpression): SQL {
66
+ try {
67
+ return this.translateExpression(expression);
68
+ } catch (error) {
69
+ throw new DrizzleTranslationError(
70
+ `Failed to translate expression: ${error instanceof Error ? error.message : String(error)}`
71
+ );
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Check if an expression can be translated to Drizzle ORM
77
+ */
78
+ public canTranslate(expression: QueryExpression): boolean {
79
+ try {
80
+ this.translateExpression(expression);
81
+ return true;
82
+ } catch {
83
+ return false;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Translate any QueryKit expression to a Drizzle ORM condition
89
+ */
90
+ private translateExpression(expression: QueryExpression): SQL {
91
+ if (!expression) {
92
+ throw new DrizzleTranslationError('Empty expression');
93
+ }
94
+
95
+ switch (expression.type) {
96
+ case 'comparison':
97
+ return this.translateComparisonExpression(expression);
98
+ case 'logical':
99
+ return this.translateLogicalExpression(expression);
100
+ default:
101
+ throw new DrizzleTranslationError(
102
+ `Unsupported expression type: ${(expression as { type: string }).type}`
103
+ );
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Translate a comparison expression to a Drizzle ORM condition
109
+ */
110
+ private translateComparisonExpression(
111
+ expression: IComparisonExpression
112
+ ): SQL {
113
+ const { field, operator, value } = expression;
114
+ const fieldName = this.normalizeField(field);
115
+
116
+ // Get the field from the schema if available
117
+ const schemaField = this.getSchemaField(fieldName);
118
+
119
+ // If we have a schema field, use it directly with Drizzle operators
120
+ if (schemaField) {
121
+ switch (operator) {
122
+ case '==':
123
+ return eq(schemaField, value);
124
+ case '!=':
125
+ return ne(schemaField, value);
126
+ case '>':
127
+ return gt(schemaField, value);
128
+ case '>=':
129
+ return gte(schemaField, value);
130
+ case '<':
131
+ return lt(schemaField, value);
132
+ case '<=':
133
+ return lte(schemaField, value);
134
+ case 'LIKE': {
135
+ if (typeof value !== 'string') {
136
+ throw new DrizzleTranslationError(
137
+ 'LIKE operator requires a string value'
138
+ );
139
+ }
140
+ // Convert wildcard to SQL pattern and use the like function
141
+ const sqlPattern = this.wildcardToSqlPattern(value);
142
+ return sql`${schemaField} LIKE ${sqlPattern}`;
143
+ }
144
+ case 'IN':
145
+ if (!Array.isArray(value)) {
146
+ throw new DrizzleTranslationError(
147
+ 'IN operator requires an array value'
148
+ );
149
+ }
150
+ return inArray(schemaField, value);
151
+ case 'NOT IN':
152
+ if (!Array.isArray(value)) {
153
+ throw new DrizzleTranslationError(
154
+ 'NOT IN operator requires an array value'
155
+ );
156
+ }
157
+ return notInArray(schemaField, value);
158
+ default:
159
+ throw new DrizzleTranslationError(
160
+ `Unsupported operator: ${operator}`
161
+ );
162
+ }
163
+ }
164
+
165
+ // If we don't have a schema field, we need to build the SQL manually
166
+ // Handle each operator type
167
+ return this.buildSqlForOperator(fieldName, operator, value);
168
+ }
169
+
170
+ /**
171
+ * Build SQL for a specific operator with raw field name
172
+ * Security: Validates field names to prevent SQL injection
173
+ */
174
+ private buildSqlForOperator(
175
+ fieldName: string,
176
+ operator: string,
177
+ value: unknown
178
+ ): SQL {
179
+ // Security fix: Validate field name format before using it (prevents SQL injection)
180
+ if (!this.isValidFieldName(fieldName)) {
181
+ throw new DrizzleTranslationError(`Invalid field name: ${fieldName}`);
182
+ }
183
+
184
+ switch (operator) {
185
+ case '==':
186
+ return sql`${sql.identifier(fieldName)} = ${value}`;
187
+ case '!=':
188
+ return sql`${sql.identifier(fieldName)} != ${value}`;
189
+ case '>':
190
+ return sql`${sql.identifier(fieldName)} > ${value}`;
191
+ case '>=':
192
+ return sql`${sql.identifier(fieldName)} >= ${value}`;
193
+ case '<':
194
+ return sql`${sql.identifier(fieldName)} < ${value}`;
195
+ case '<=':
196
+ return sql`${sql.identifier(fieldName)} <= ${value}`;
197
+ case 'LIKE': {
198
+ if (typeof value !== 'string') {
199
+ throw new DrizzleTranslationError(
200
+ 'LIKE operator requires a string value'
201
+ );
202
+ }
203
+ // Convert wildcard to SQL pattern
204
+ const sqlPattern = this.wildcardToSqlPattern(value);
205
+ return sql`${sql.identifier(fieldName)} LIKE ${sqlPattern}`;
206
+ }
207
+ case 'IN': {
208
+ if (!Array.isArray(value)) {
209
+ throw new DrizzleTranslationError(
210
+ 'IN operator requires an array value'
211
+ );
212
+ }
213
+ if (value.length === 0) {
214
+ // Empty IN clause should always be false
215
+ return sql`FALSE`;
216
+ }
217
+ return sql`${sql.identifier(fieldName)} IN (${value})`;
218
+ }
219
+ case 'NOT IN': {
220
+ if (!Array.isArray(value)) {
221
+ throw new DrizzleTranslationError(
222
+ 'NOT IN operator requires an array value'
223
+ );
224
+ }
225
+ if (value.length === 0) {
226
+ // Empty NOT IN clause should always be true
227
+ return sql`TRUE`;
228
+ }
229
+ return sql`${sql.identifier(fieldName)} NOT IN (${value})`;
230
+ }
231
+ default:
232
+ throw new DrizzleTranslationError(`Unsupported operator: ${operator}`);
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Convert wildcard pattern to SQL LIKE pattern
238
+ */
239
+ private wildcardToSqlPattern(pattern: string): string {
240
+ // Replace * with % and ? with _ for SQL LIKE syntax
241
+ // Also escape any existing SQL LIKE special characters
242
+ return pattern
243
+ .replace(/%/g, '\\%') // Escape existing %
244
+ .replace(/_/g, '\\_') // Escape existing _
245
+ .replace(/\*/g, '%') // * → %
246
+ .replace(/\?/g, '_'); // ? → _
247
+ }
248
+
249
+ /**
250
+ * Translate a logical expression to a Drizzle ORM condition
251
+ */
252
+ private translateLogicalExpression(expression: ILogicalExpression): SQL {
253
+ const { operator, left, right } = expression;
254
+
255
+ const leftSql = this.translateExpression(left);
256
+
257
+ if (operator === 'NOT') {
258
+ return sql`NOT (${leftSql})`;
259
+ }
260
+
261
+ if (!right) {
262
+ throw new DrizzleTranslationError(
263
+ `${operator} operator requires two operands`
264
+ );
265
+ }
266
+
267
+ const rightSql = this.translateExpression(right);
268
+
269
+ switch (operator) {
270
+ case 'AND':
271
+ return sql`(${leftSql}) AND (${rightSql})`;
272
+ case 'OR':
273
+ return sql`(${leftSql}) OR (${rightSql})`;
274
+ default:
275
+ throw new DrizzleTranslationError(
276
+ `Unsupported logical operator: ${operator}`
277
+ );
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Normalize a field name based on translator options
283
+ */
284
+ private normalizeField(field: string): string {
285
+ const normalizedField = this.options.normalizeFieldNames
286
+ ? field.toLowerCase()
287
+ : field;
288
+
289
+ return this.options.fieldMappings[normalizedField] ?? normalizedField;
290
+ }
291
+
292
+ /**
293
+ * Get a field from the schema if it exists
294
+ */
295
+ private getSchemaField(fieldName: string): SQLWrapper | null {
296
+ // Extract table and column names from fieldName (e.g., 'users.id' -> { table: 'users', column: 'id' })
297
+ const parts = fieldName.split('.');
298
+
299
+ if (parts.length === 2) {
300
+ const [tableName, columnName] = parts;
301
+ const table = this.options.schema[tableName] as
302
+ | Record<string, unknown>
303
+ | undefined;
304
+
305
+ if (table && columnName in table) {
306
+ return table[columnName] as SQLWrapper;
307
+ }
308
+ }
309
+
310
+ // Heuristic: if there's only one table in the schema, allow bare field lookup
311
+ if (parts.length === 1) {
312
+ const [onlyTableName] = Object.keys(this.options.schema);
313
+ if (onlyTableName) {
314
+ const table = this.options.schema[onlyTableName] as
315
+ | Record<string, unknown>
316
+ | undefined;
317
+ const columnName = parts[0];
318
+ if (table && columnName in table) {
319
+ return table[columnName] as SQLWrapper;
320
+ }
321
+ }
322
+ }
323
+
324
+ // If the field is not found in the schema
325
+ return null;
326
+ }
327
+
328
+ /**
329
+ * Security: Validates field names to prevent SQL injection
330
+ * Only allows alphanumeric chars, dots, underscores. Max 64 chars per part.
331
+ */
332
+ private isValidFieldName(fieldName: string): boolean {
333
+ const validFieldPattern = /^[a-zA-Z][a-zA-Z0-9._]*$/;
334
+ const parts = fieldName.split('.');
335
+
336
+ // Only allow table.column format (max 2 parts)
337
+ if (parts.length > 2) return false;
338
+
339
+ return parts.every(
340
+ part =>
341
+ validFieldPattern.test(part) &&
342
+ part.length <= 64 &&
343
+ !part.includes('__') // Prevent reserved patterns
344
+ );
345
+ }
346
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * QueryKit Translators
3
+ *
4
+ * Exports all translator implementations for different target platforms.
5
+ */
6
+
7
+ // Export base types
8
+ export * from './types';
9
+
10
+ // Export Drizzle translator
11
+ export * from './drizzle';
12
+
13
+ // Export SQL translator
14
+ export * from './sql';
@@ -0,0 +1,45 @@
1
+ import { SqlTranslator } from './index';
2
+
3
+ describe('SqlTranslator', () => {
4
+ let translator: SqlTranslator;
5
+
6
+ beforeEach(() => {
7
+ translator = new SqlTranslator();
8
+ });
9
+
10
+ // Helper function to access private wildcardToSqlPattern method
11
+ function testWildcardPattern(pattern: string): string {
12
+ // We need to access a private method for testing - using type assertion
13
+ return (translator as unknown as {
14
+ wildcardToSqlPattern: (p: string) => string
15
+ }).wildcardToSqlPattern(pattern);
16
+ }
17
+
18
+ // Other tests...
19
+
20
+ describe('wildcardToSqlPattern', () => {
21
+ it('should convert * wildcard to % SQL pattern', () => {
22
+ expect(testWildcardPattern('foo*')).toBe('foo%');
23
+ expect(testWildcardPattern('*bar')).toBe('%bar');
24
+ expect(testWildcardPattern('foo*bar')).toBe('foo%bar');
25
+ expect(testWildcardPattern('*foo*')).toBe('%foo%');
26
+ });
27
+
28
+ it('should convert ? wildcard to _ SQL pattern', () => {
29
+ expect(testWildcardPattern('foo?')).toBe('foo_');
30
+ expect(testWildcardPattern('?bar')).toBe('_bar');
31
+ expect(testWildcardPattern('foo?bar')).toBe('foo_bar');
32
+ });
33
+
34
+ it('should handle mixed wildcards', () => {
35
+ expect(testWildcardPattern('f*o?bar*')).toBe('f%o_bar%');
36
+ expect(testWildcardPattern('*test?')).toBe('%test_');
37
+ });
38
+
39
+ it('should escape existing SQL special characters', () => {
40
+ expect(testWildcardPattern('foo%bar')).toBe('foo\\%bar');
41
+ expect(testWildcardPattern('foo_bar')).toBe('foo\\_bar');
42
+ expect(testWildcardPattern('foo_%bar*')).toBe('foo\\_\\%bar%');
43
+ });
44
+ });
45
+ });