@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,419 @@
|
|
|
1
|
+
import { QueryParser } from '../../parser';
|
|
2
|
+
import { QueryExpression } from '../../parser/types';
|
|
3
|
+
import {
|
|
4
|
+
SqlTranslator,
|
|
5
|
+
SqlTranslationError,
|
|
6
|
+
ISqlTranslationResult
|
|
7
|
+
} from './index';
|
|
8
|
+
|
|
9
|
+
describe('SqlTranslator', () => {
|
|
10
|
+
const translator = new SqlTranslator({ useParameters: false });
|
|
11
|
+
const parser = new QueryParser();
|
|
12
|
+
|
|
13
|
+
describe('translate', () => {
|
|
14
|
+
it('should translate a simple equality expression', () => {
|
|
15
|
+
const expression = parser.parse('name:"John"');
|
|
16
|
+
const result = translator.translate(expression) as string;
|
|
17
|
+
|
|
18
|
+
expect(result).toBe('"name" = \'John\'');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should translate a comparison expression', () => {
|
|
22
|
+
const expression = parser.parse('priority:>18');
|
|
23
|
+
const result = translator.translate(expression) as string;
|
|
24
|
+
|
|
25
|
+
expect(result).toBe('"priority" > 18');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should translate numeric values directly', () => {
|
|
29
|
+
const expression = parser.parse('count:42');
|
|
30
|
+
const result = translator.translate(expression) as string;
|
|
31
|
+
|
|
32
|
+
expect(result).toBe('"count" = 42');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should translate boolean values correctly', () => {
|
|
36
|
+
const expression = parser.parse('active:true');
|
|
37
|
+
const result = translator.translate(expression) as string;
|
|
38
|
+
|
|
39
|
+
expect(result).toBe('"active" = true');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should translate a simple comparison expression', () => {
|
|
43
|
+
const expression = parser.parse('priority:>2');
|
|
44
|
+
const result = translator.translate(expression) as string;
|
|
45
|
+
|
|
46
|
+
expect(result).toBe('"priority" > 2');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should translate a logical AND expression', () => {
|
|
50
|
+
const expression = parser.parse('priority:>2 AND status:"active"');
|
|
51
|
+
const result = translator.translate(expression) as string;
|
|
52
|
+
|
|
53
|
+
expect(result).toBe('("priority" > 2) AND ("status" = \'active\')');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should translate a logical OR expression', () => {
|
|
57
|
+
const expression = parser.parse('status:"active" OR status:"pending"');
|
|
58
|
+
const result = translator.translate(expression) as string;
|
|
59
|
+
|
|
60
|
+
expect(result).toBe(
|
|
61
|
+
'("status" = \'active\') OR ("status" = \'pending\')'
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should translate a NOT expression', () => {
|
|
66
|
+
const expression = parser.parse('NOT status:"inactive"');
|
|
67
|
+
const result = translator.translate(expression) as string;
|
|
68
|
+
|
|
69
|
+
expect(result).toBe('NOT ("status" = \'inactive\')');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should translate a complex nested expression', () => {
|
|
73
|
+
const expression = parser.parse(
|
|
74
|
+
'(priority:>2 AND status:"active") OR (role:"admin")'
|
|
75
|
+
);
|
|
76
|
+
const result = translator.translate(expression) as string;
|
|
77
|
+
|
|
78
|
+
expect(result).toBe(
|
|
79
|
+
'(("priority" > 2) AND ("status" = \'active\')) OR ("role" = \'admin\')'
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should handle null values correctly', () => {
|
|
84
|
+
// Create a comparison expression with null value
|
|
85
|
+
const expression: QueryExpression = {
|
|
86
|
+
type: 'comparison',
|
|
87
|
+
field: 'last_login',
|
|
88
|
+
operator: '==',
|
|
89
|
+
value: null
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const result = translator.translate(expression) as string;
|
|
93
|
+
expect(result).toBe('"last_login" IS NULL');
|
|
94
|
+
|
|
95
|
+
// Not equals with null
|
|
96
|
+
const notNullExpression: QueryExpression = {
|
|
97
|
+
type: 'comparison',
|
|
98
|
+
field: 'last_login',
|
|
99
|
+
operator: '!=',
|
|
100
|
+
value: null
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const notNullResult = translator.translate(notNullExpression) as string;
|
|
104
|
+
expect(notNullResult).toBe('"last_login" IS NOT NULL');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should handle IN operator with array values', () => {
|
|
108
|
+
// Create an IN expression
|
|
109
|
+
const expression: QueryExpression = {
|
|
110
|
+
type: 'comparison',
|
|
111
|
+
field: 'status',
|
|
112
|
+
operator: 'IN',
|
|
113
|
+
value: ['active', 'pending', 'reviewing']
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const result = translator.translate(expression) as string;
|
|
117
|
+
expect(result).toBe("\"status\" IN ('active', 'pending', 'reviewing')");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should handle NOT IN operator with array values', () => {
|
|
121
|
+
// Create a NOT IN expression
|
|
122
|
+
const expression: QueryExpression = {
|
|
123
|
+
type: 'comparison',
|
|
124
|
+
field: 'status',
|
|
125
|
+
operator: 'NOT IN',
|
|
126
|
+
value: ['inactive', 'closed']
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const result = translator.translate(expression) as string;
|
|
130
|
+
expect(result).toBe("\"status\" NOT IN ('inactive', 'closed')");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should handle empty IN arrays properly', () => {
|
|
134
|
+
const expression: QueryExpression = {
|
|
135
|
+
type: 'comparison',
|
|
136
|
+
field: 'status',
|
|
137
|
+
operator: 'IN',
|
|
138
|
+
value: []
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const result = translator.translate(expression) as string;
|
|
142
|
+
expect(result).toBe('FALSE');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should handle empty NOT IN arrays properly', () => {
|
|
146
|
+
const expression: QueryExpression = {
|
|
147
|
+
type: 'comparison',
|
|
148
|
+
field: 'status',
|
|
149
|
+
operator: 'NOT IN',
|
|
150
|
+
value: []
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const result = translator.translate(expression) as string;
|
|
154
|
+
expect(result).toBe('TRUE');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should handle table.column notation', () => {
|
|
158
|
+
const expression = parser.parse('users.priority:>2');
|
|
159
|
+
const result = translator.translate(expression) as string;
|
|
160
|
+
|
|
161
|
+
expect(result).toBe('"users"."priority" > 2');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should properly escape string values with quotes', () => {
|
|
165
|
+
const expression = parser.parse('message:"It\'s a test"');
|
|
166
|
+
const result = translator.translate(expression) as string;
|
|
167
|
+
|
|
168
|
+
expect(result).toBe("\"message\" = 'It''s a test'");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should translate a combined expression with AND', () => {
|
|
172
|
+
const expression = parser.parse('priority:>2 AND status:"active"');
|
|
173
|
+
const sql = translator.translate(expression);
|
|
174
|
+
|
|
175
|
+
expect(sql).toContain('"priority" > 2');
|
|
176
|
+
expect(sql).toContain('"status" = ');
|
|
177
|
+
expect(sql).toContain('AND');
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('with different quote options', () => {
|
|
182
|
+
it('should use custom identifier quotes', () => {
|
|
183
|
+
const mysqlTranslator = new SqlTranslator({
|
|
184
|
+
identifierQuote: '`',
|
|
185
|
+
useParameters: false
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const expression = parser.parse('name:"John"');
|
|
189
|
+
const result = mysqlTranslator.translate(expression) as string;
|
|
190
|
+
|
|
191
|
+
expect(result).toBe("`name` = 'John'");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should use custom string literal quotes', () => {
|
|
195
|
+
const mssqlTranslator = new SqlTranslator({
|
|
196
|
+
stringLiteralQuote: '"',
|
|
197
|
+
useParameters: false
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const expression = parser.parse('name:"John"');
|
|
201
|
+
const result = mssqlTranslator.translate(expression) as string;
|
|
202
|
+
|
|
203
|
+
expect(result).toBe('"name" = "John"');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('with parameters', () => {
|
|
208
|
+
const paramTranslator = new SqlTranslator({
|
|
209
|
+
useParameters: true
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should use parameters for simple values', () => {
|
|
213
|
+
const expression = parser.parse('name:"John"');
|
|
214
|
+
const result = paramTranslator.translate(
|
|
215
|
+
expression
|
|
216
|
+
) as ISqlTranslationResult;
|
|
217
|
+
|
|
218
|
+
expect(result.sql).toBe('"name" = ?');
|
|
219
|
+
expect(result.params).toEqual(['John']);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should use parameters for comparison operators', () => {
|
|
223
|
+
const expression = parser.parse('priority:>18');
|
|
224
|
+
const result = paramTranslator.translate(
|
|
225
|
+
expression
|
|
226
|
+
) as ISqlTranslationResult;
|
|
227
|
+
|
|
228
|
+
expect(result.sql).toBe('"priority" > ?');
|
|
229
|
+
expect(result.params).toEqual([18]);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should use parameters for complex expressions', () => {
|
|
233
|
+
const expression = parser.parse(
|
|
234
|
+
'(priority:>18 AND status:"active") OR (role:"admin")'
|
|
235
|
+
);
|
|
236
|
+
const result = paramTranslator.translate(
|
|
237
|
+
expression
|
|
238
|
+
) as ISqlTranslationResult;
|
|
239
|
+
|
|
240
|
+
expect(result.sql).toBe(
|
|
241
|
+
'(("priority" > ?) AND ("status" = ?)) OR ("role" = ?)'
|
|
242
|
+
);
|
|
243
|
+
expect(result.params).toEqual([18, 'active', 'admin']);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should use parameters for IN clauses', () => {
|
|
247
|
+
const expression: QueryExpression = {
|
|
248
|
+
type: 'comparison',
|
|
249
|
+
field: 'status',
|
|
250
|
+
operator: 'IN',
|
|
251
|
+
value: ['active', 'pending']
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const result = paramTranslator.translate(
|
|
255
|
+
expression
|
|
256
|
+
) as ISqlTranslationResult;
|
|
257
|
+
expect(result.sql).toBe('"status" IN (?, ?)');
|
|
258
|
+
expect(result.params).toEqual(['active', 'pending']);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe('with field mappings', () => {
|
|
263
|
+
const mappingTranslator = new SqlTranslator({
|
|
264
|
+
fieldMappings: {
|
|
265
|
+
user_id: 'users.id',
|
|
266
|
+
user_name: 'users.name'
|
|
267
|
+
},
|
|
268
|
+
useParameters: false
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should apply field mappings', () => {
|
|
272
|
+
const expression = parser.parse('user_id:123');
|
|
273
|
+
const result = mappingTranslator.translate(expression) as string;
|
|
274
|
+
|
|
275
|
+
expect(result).toBe('"users"."id" = 123');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should apply field mappings in complex expressions', () => {
|
|
279
|
+
const expression = parser.parse('user_id:>100 AND user_name:"John"');
|
|
280
|
+
const result = mappingTranslator.translate(expression) as string;
|
|
281
|
+
|
|
282
|
+
expect(result).toBe(
|
|
283
|
+
'("users"."id" > 100) AND ("users"."name" = \'John\')'
|
|
284
|
+
);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe('with table prefixes', () => {
|
|
289
|
+
it('should preserve table prefixes in field names', () => {
|
|
290
|
+
const expression = parser.parse('users.priority:>2');
|
|
291
|
+
const sql = translator.translate(expression);
|
|
292
|
+
|
|
293
|
+
expect(sql).toContain('"users"."priority"');
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe('canTranslate', () => {
|
|
298
|
+
it('should return true for valid expressions', () => {
|
|
299
|
+
const expression = parser.parse('priority:>2');
|
|
300
|
+
expect(translator.canTranslate(expression)).toBe(true);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('should return false for unsupported expressions', () => {
|
|
304
|
+
// Create a malformed expression to test error handling
|
|
305
|
+
const invalidExpression = {
|
|
306
|
+
type: 'unsupported'
|
|
307
|
+
} as unknown as QueryExpression;
|
|
308
|
+
expect(translator.canTranslate(invalidExpression)).toBe(false);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('should return false for invalid operators', () => {
|
|
312
|
+
const invalidExpression = {
|
|
313
|
+
type: 'comparison',
|
|
314
|
+
field: 'name',
|
|
315
|
+
operator: '=~', // Invalid operator
|
|
316
|
+
value: 'pattern'
|
|
317
|
+
} as unknown as QueryExpression;
|
|
318
|
+
|
|
319
|
+
expect(translator.canTranslate(invalidExpression)).toBe(false);
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
describe('error handling', () => {
|
|
324
|
+
it('should throw error for empty expressions', () => {
|
|
325
|
+
expect(() =>
|
|
326
|
+
translator.translate(null as unknown as QueryExpression)
|
|
327
|
+
).toThrow(SqlTranslationError);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should throw error for invalid expression types', () => {
|
|
331
|
+
const invalidExpression = {
|
|
332
|
+
type: 'unknown'
|
|
333
|
+
} as unknown as QueryExpression;
|
|
334
|
+
|
|
335
|
+
expect(() => translator.translate(invalidExpression)).toThrow(
|
|
336
|
+
SqlTranslationError
|
|
337
|
+
);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('should throw error for invalid operator', () => {
|
|
341
|
+
const invalidExpression = {
|
|
342
|
+
type: 'comparison',
|
|
343
|
+
field: 'name',
|
|
344
|
+
operator: '=~', // Invalid operator
|
|
345
|
+
value: 'pattern'
|
|
346
|
+
} as unknown as QueryExpression;
|
|
347
|
+
|
|
348
|
+
expect(() => translator.translate(invalidExpression)).toThrow(
|
|
349
|
+
SqlTranslationError
|
|
350
|
+
);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('should throw error for IN operator with non-array value', () => {
|
|
354
|
+
const invalidExpression: QueryExpression = {
|
|
355
|
+
type: 'comparison',
|
|
356
|
+
field: 'status',
|
|
357
|
+
operator: 'IN',
|
|
358
|
+
value: 'not-an-array'
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
expect(() => translator.translate(invalidExpression)).toThrow(
|
|
362
|
+
SqlTranslationError
|
|
363
|
+
);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('should throw error for invalid value types', () => {
|
|
367
|
+
// Access the private formatValue method for direct testing
|
|
368
|
+
const formatValue = translator['formatValue'].bind(translator);
|
|
369
|
+
|
|
370
|
+
// Test with a complex object (which is not a valid QueryValue but might be passed incorrectly)
|
|
371
|
+
expect(() => formatValue({ complex: 'object' })).toThrow(
|
|
372
|
+
SqlTranslationError
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
// Test with a symbol (also not a valid QueryValue)
|
|
376
|
+
expect(() => formatValue(Symbol('test'))).toThrow(SqlTranslationError);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('should throw error for logical expressions without right operand', () => {
|
|
380
|
+
const invalidExpression: QueryExpression = {
|
|
381
|
+
type: 'logical',
|
|
382
|
+
operator: 'AND',
|
|
383
|
+
left: {
|
|
384
|
+
type: 'comparison',
|
|
385
|
+
field: 'name',
|
|
386
|
+
operator: '==',
|
|
387
|
+
value: 'John'
|
|
388
|
+
}
|
|
389
|
+
// Missing right operand
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
expect(() => translator.translate(invalidExpression)).toThrow(
|
|
393
|
+
SqlTranslationError
|
|
394
|
+
);
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
describe('with options', () => {
|
|
399
|
+
it('should allow customization of output format', () => {
|
|
400
|
+
const expression = parser.parse('priority:>2');
|
|
401
|
+
const paramTranslator = new SqlTranslator({ useParameters: true });
|
|
402
|
+
const result = paramTranslator.translate(expression);
|
|
403
|
+
|
|
404
|
+
expect(typeof result).toBe('object');
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('should handle complex expressions with parameterization', () => {
|
|
408
|
+
const expr = '(priority:>2 AND status:"active") OR (role:"admin")';
|
|
409
|
+
const expression = parser.parse(expr);
|
|
410
|
+
const paramTranslator = new SqlTranslator({ useParameters: true });
|
|
411
|
+
const result = paramTranslator.translate(
|
|
412
|
+
expression
|
|
413
|
+
) as ISqlTranslationResult;
|
|
414
|
+
|
|
415
|
+
expect(typeof result).toBe('object');
|
|
416
|
+
expect(Array.isArray(result.params)).toBe(true);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
|
|
8
|
+
import { QueryExpression } from '../parser/types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Options for configuring a translator
|
|
12
|
+
*/
|
|
13
|
+
export interface ITranslatorOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Whether to normalize field names (e.g., lowercase them)
|
|
16
|
+
*/
|
|
17
|
+
normalizeFieldNames?: boolean;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Custom field mappings from QueryKit fields to target fields
|
|
21
|
+
*/
|
|
22
|
+
fieldMappings?: Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Interface for a query translator
|
|
27
|
+
*/
|
|
28
|
+
export interface ITranslator<T = unknown> {
|
|
29
|
+
/**
|
|
30
|
+
* Translate a QueryKit expression into the target format
|
|
31
|
+
*
|
|
32
|
+
* @param expression The QueryKit expression to translate
|
|
33
|
+
* @returns The translated query in the target format
|
|
34
|
+
*/
|
|
35
|
+
translate(expression: QueryExpression): T;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if an expression can be translated
|
|
39
|
+
*
|
|
40
|
+
* @param expression The expression to check
|
|
41
|
+
* @returns true if the expression can be translated, false otherwise
|
|
42
|
+
*/
|
|
43
|
+
canTranslate(expression: QueryExpression): boolean;
|
|
44
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"noImplicitAny": true,
|
|
11
|
+
"strictNullChecks": true,
|
|
12
|
+
"strictFunctionTypes": true,
|
|
13
|
+
"strictBindCallApply": true,
|
|
14
|
+
"strictPropertyInitialization": true,
|
|
15
|
+
"noImplicitThis": true,
|
|
16
|
+
"alwaysStrict": true,
|
|
17
|
+
"noUnusedLocals": true,
|
|
18
|
+
"noUnusedParameters": true,
|
|
19
|
+
"noImplicitReturns": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"moduleResolution": "node",
|
|
22
|
+
"baseUrl": "./",
|
|
23
|
+
"paths": {
|
|
24
|
+
"*": ["node_modules/*"]
|
|
25
|
+
},
|
|
26
|
+
"esModuleInterop": true,
|
|
27
|
+
"experimentalDecorators": true,
|
|
28
|
+
"emitDecoratorMetadata": true,
|
|
29
|
+
"skipLibCheck": true,
|
|
30
|
+
"forceConsistentCasingInFileNames": true
|
|
31
|
+
},
|
|
32
|
+
"include": ["src/**/*"],
|
|
33
|
+
"exclude": ["node_modules", "**/*.test.ts"]
|
|
34
|
+
}
|