@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,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security tests validating fixes for critical vulnerabilities:
|
|
3
|
+
* - SQL injection via field names
|
|
4
|
+
* - Field enumeration attacks
|
|
5
|
+
* - ReDoS via wildcard patterns
|
|
6
|
+
* - Type confusion bypasses
|
|
7
|
+
* - NoSQL injection via objects
|
|
8
|
+
*/
|
|
9
|
+
import { QueryParser, QueryParseError } from './parser/parser';
|
|
10
|
+
import {
|
|
11
|
+
QuerySecurityValidator,
|
|
12
|
+
QuerySecurityError
|
|
13
|
+
} from './security/validator';
|
|
14
|
+
import {
|
|
15
|
+
DrizzleTranslator,
|
|
16
|
+
DrizzleTranslationError
|
|
17
|
+
} from './translators/drizzle';
|
|
18
|
+
|
|
19
|
+
describe('Security Audit Tests', () => {
|
|
20
|
+
let parser: QueryParser;
|
|
21
|
+
let validator: QuerySecurityValidator;
|
|
22
|
+
let translator: DrizzleTranslator;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
parser = new QueryParser();
|
|
26
|
+
validator = new QuerySecurityValidator({
|
|
27
|
+
allowedFields: [
|
|
28
|
+
'name',
|
|
29
|
+
'email',
|
|
30
|
+
'priority',
|
|
31
|
+
'status',
|
|
32
|
+
'field',
|
|
33
|
+
'active',
|
|
34
|
+
'count',
|
|
35
|
+
'percentage',
|
|
36
|
+
'a',
|
|
37
|
+
'b',
|
|
38
|
+
'c',
|
|
39
|
+
'd',
|
|
40
|
+
'e',
|
|
41
|
+
'f',
|
|
42
|
+
'g'
|
|
43
|
+
],
|
|
44
|
+
denyFields: ['password', 'secret'],
|
|
45
|
+
maxValueLength: 50,
|
|
46
|
+
maxQueryDepth: 3,
|
|
47
|
+
maxClauseCount: 5
|
|
48
|
+
});
|
|
49
|
+
translator = new DrizzleTranslator();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('VULN-001: SQL Injection via Raw SQL Construction', () => {
|
|
53
|
+
it('should handle malicious field names safely', () => {
|
|
54
|
+
const maliciousQueries = [
|
|
55
|
+
'user.name; DROP TABLE users; --:"test"',
|
|
56
|
+
'id\'; DELETE FROM users; --:"1"',
|
|
57
|
+
'name` OR 1=1; --:"admin"',
|
|
58
|
+
'field); DROP TABLE users;--:"value"'
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
maliciousQueries.forEach(query => {
|
|
62
|
+
try {
|
|
63
|
+
const parsed = parser.parse(query);
|
|
64
|
+
expect(() => validator.validate(parsed)).toThrow(QuerySecurityError);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
// Parser should either reject or validator should catch
|
|
67
|
+
expect(error).toBeInstanceOf(QueryParseError);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should prevent SQL injection through field names in translator', () => {
|
|
73
|
+
// Create a malicious expression that bypasses parser validation
|
|
74
|
+
const maliciousExpression = {
|
|
75
|
+
type: 'comparison' as const,
|
|
76
|
+
field: 'user.id; DROP TABLE users; --',
|
|
77
|
+
operator: '==' as const,
|
|
78
|
+
value: 'test'
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
expect(() => translator.translate(maliciousExpression)).toThrow(
|
|
82
|
+
DrizzleTranslationError
|
|
83
|
+
);
|
|
84
|
+
expect(() => translator.translate(maliciousExpression)).toThrow(
|
|
85
|
+
'Invalid field name'
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('VULN-002: Field Enumeration via Error Messages', () => {
|
|
91
|
+
it('should not reveal field existence through error messages', () => {
|
|
92
|
+
const validatorStrict = new QuerySecurityValidator({
|
|
93
|
+
allowedFields: ['name', 'email'],
|
|
94
|
+
denyFields: ['password']
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const unauthorizedQueries = [
|
|
98
|
+
'password:"secret"',
|
|
99
|
+
'nonexistent:"value"',
|
|
100
|
+
'hidden_field:"data"'
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
unauthorizedQueries.forEach(query => {
|
|
104
|
+
const parsed = parser.parse(query);
|
|
105
|
+
expect(() => validatorStrict.validate(parsed)).toThrow(
|
|
106
|
+
'Invalid query parameters'
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should use generic error messages for denied fields', () => {
|
|
112
|
+
const deniedQuery = parser.parse('password:"secret"');
|
|
113
|
+
|
|
114
|
+
expect(() => validator.validate(deniedQuery)).toThrow(QuerySecurityError);
|
|
115
|
+
expect(() => validator.validate(deniedQuery)).toThrow(
|
|
116
|
+
'Invalid query parameters'
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('VULN-003: ReDoS via Wildcard Patterns', () => {
|
|
122
|
+
it('should prevent catastrophic backtracking patterns', () => {
|
|
123
|
+
const redosPatterns = [
|
|
124
|
+
'name:"*a*a*a*a*a*a*a*a*a*a*b"',
|
|
125
|
+
'name:"?x?x?x?x?x?x?x?x?x?x?y"'
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
redosPatterns.forEach(pattern => {
|
|
129
|
+
const parsed = parser.parse(pattern);
|
|
130
|
+
expect(() => validator.validate(parsed)).toThrow(QuerySecurityError);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should limit wildcard usage', () => {
|
|
135
|
+
const excessiveWildcards = 'name:"' + '*'.repeat(15) + '"';
|
|
136
|
+
const parsed = parser.parse(excessiveWildcards);
|
|
137
|
+
|
|
138
|
+
expect(() => validator.validate(parsed)).toThrow(
|
|
139
|
+
'Excessive wildcard usage'
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should sanitize consecutive wildcards', () => {
|
|
144
|
+
const multiWildcard = parser.parse('name:"***test***"');
|
|
145
|
+
|
|
146
|
+
// Should either sanitize or reject based on pattern complexity
|
|
147
|
+
try {
|
|
148
|
+
validator.validate(multiWildcard);
|
|
149
|
+
if (multiWildcard.type === 'comparison') {
|
|
150
|
+
expect(multiWildcard.value).toBe('*test*');
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
expect(error).toBeInstanceOf(QuerySecurityError);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('VULN-004: Logic Bypass via Type Confusion', () => {
|
|
159
|
+
it('should validate array value lengths', () => {
|
|
160
|
+
const longStringArray = {
|
|
161
|
+
type: 'comparison' as const,
|
|
162
|
+
field: 'status',
|
|
163
|
+
operator: 'IN' as const,
|
|
164
|
+
value: ['a'.repeat(100), 'b'.repeat(100)]
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
expect(() => validator.validate(longStringArray)).toThrow(
|
|
168
|
+
QuerySecurityError
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should limit array sizes', () => {
|
|
173
|
+
const largeArray = {
|
|
174
|
+
type: 'comparison' as const,
|
|
175
|
+
field: 'status',
|
|
176
|
+
operator: 'IN' as const,
|
|
177
|
+
value: Array(150).fill('test')
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
expect(() => validator.validate(largeArray)).toThrow(
|
|
181
|
+
'Array values cannot exceed 100 items'
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should prevent object values in arrays', () => {
|
|
186
|
+
const objectInArray = {
|
|
187
|
+
type: 'comparison' as const,
|
|
188
|
+
field: 'status',
|
|
189
|
+
operator: 'IN' as const,
|
|
190
|
+
value: ['test', { malicious: 'object' }] as unknown as Array<
|
|
191
|
+
string | number | boolean | null
|
|
192
|
+
>
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
expect(() => validator.validate(objectInArray)).toThrow(
|
|
196
|
+
'Object values are not allowed'
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('VULN-005: NoSQL Injection via Object Values', () => {
|
|
202
|
+
it('should reject object values in parser', () => {
|
|
203
|
+
// Simulate object injection attempt
|
|
204
|
+
const objectValue = { $ne: null };
|
|
205
|
+
|
|
206
|
+
expect(() => parser['convertLiqeValue'](objectValue)).toThrow(
|
|
207
|
+
QueryParseError
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should prevent object injection through complex values', () => {
|
|
212
|
+
const maliciousExpression = {
|
|
213
|
+
type: 'comparison' as const,
|
|
214
|
+
field: 'user',
|
|
215
|
+
operator: '==' as const,
|
|
216
|
+
value: { $where: 'this.password.length > 0' } as unknown as
|
|
217
|
+
| string
|
|
218
|
+
| number
|
|
219
|
+
| boolean
|
|
220
|
+
| null
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
expect(() => validator.validate(maliciousExpression)).toThrow(
|
|
224
|
+
QuerySecurityError
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('Query Complexity Limits', () => {
|
|
230
|
+
it('should enforce maximum query depth', () => {
|
|
231
|
+
// Create a query with nested logical operations that exceeds depth limit of 3
|
|
232
|
+
// Each AND/OR/NOT adds to depth, not parentheses
|
|
233
|
+
const deepQuery = parser.parse(
|
|
234
|
+
'a:1 AND (b:2 AND (c:3 AND (d:4 AND e:5)))'
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
expect(() => validator.validate(deepQuery)).toThrow(
|
|
238
|
+
'Query exceeds maximum depth'
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should enforce maximum clause count', () => {
|
|
243
|
+
// Create validator with lower clause count for this test
|
|
244
|
+
const strictValidator = new QuerySecurityValidator({
|
|
245
|
+
allowedFields: ['a', 'b', 'c', 'd', 'e', 'f', 'g'],
|
|
246
|
+
maxClauseCount: 3,
|
|
247
|
+
maxQueryDepth: 10 // Higher depth so clause count limit hits first
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const complexQuery = parser.parse('a:1 AND b:2 AND c:3 AND d:4');
|
|
251
|
+
|
|
252
|
+
expect(() => strictValidator.validate(complexQuery)).toThrow(
|
|
253
|
+
'Query exceeds maximum clause count'
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe('Input Sanitization', () => {
|
|
259
|
+
it('should handle Unicode and special characters safely', () => {
|
|
260
|
+
const unicodeQueries = [
|
|
261
|
+
'name:"\\u0000"', // Null byte
|
|
262
|
+
'name:"\\u001F"', // Control character
|
|
263
|
+
'name:"𝐇𝐞𝐥𝐥𝐨"', // Unicode mathematical bold
|
|
264
|
+
'name:"<script>alert(1)</script>"' // XSS attempt
|
|
265
|
+
];
|
|
266
|
+
|
|
267
|
+
unicodeQueries.forEach(query => {
|
|
268
|
+
const parsed = parser.parse(query);
|
|
269
|
+
expect(() => validator.validate(parsed)).not.toThrow();
|
|
270
|
+
|
|
271
|
+
// Should be handled safely by translator
|
|
272
|
+
expect(() => translator.translate(parsed)).not.toThrow();
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should handle extremely long values', () => {
|
|
277
|
+
const longValue = 'a'.repeat(2000);
|
|
278
|
+
const longQuery = parser.parse(`name:"${longValue}"`);
|
|
279
|
+
|
|
280
|
+
expect(() => validator.validate(longQuery)).toThrow(
|
|
281
|
+
'exceeds maximum length'
|
|
282
|
+
);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe('Edge Cases and Boundary Conditions', () => {
|
|
287
|
+
it('should handle empty queries safely', () => {
|
|
288
|
+
expect(() => parser.parse('')).toThrow(QueryParseError);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should handle null and undefined values', () => {
|
|
292
|
+
const nullQuery = parser.parse('field:null');
|
|
293
|
+
expect(() => validator.validate(nullQuery)).not.toThrow();
|
|
294
|
+
expect(() => translator.translate(nullQuery)).not.toThrow();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should handle boolean values correctly', () => {
|
|
298
|
+
const boolQuery = parser.parse('active:true');
|
|
299
|
+
expect(() => validator.validate(boolQuery)).not.toThrow();
|
|
300
|
+
expect(() => translator.translate(boolQuery)).not.toThrow();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('should handle numeric edge cases', () => {
|
|
304
|
+
const numericQueries = [
|
|
305
|
+
'count:0',
|
|
306
|
+
'count:-1',
|
|
307
|
+
'count:999999999999999',
|
|
308
|
+
'percentage:0.0001'
|
|
309
|
+
];
|
|
310
|
+
|
|
311
|
+
numericQueries.forEach(query => {
|
|
312
|
+
const parsed = parser.parse(query);
|
|
313
|
+
expect(() => validator.validate(parsed)).not.toThrow();
|
|
314
|
+
expect(() => translator.translate(parsed)).not.toThrow();
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe('Performance and DoS Protection', () => {
|
|
320
|
+
it('should handle deeply nested parentheses', () => {
|
|
321
|
+
const nested = '(' + 'name:"test"' + ')'.repeat(20);
|
|
322
|
+
|
|
323
|
+
expect(() => parser.parse(nested)).toThrow();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should prevent memory exhaustion via large arrays', () => {
|
|
327
|
+
const hugeArray = {
|
|
328
|
+
type: 'comparison' as const,
|
|
329
|
+
field: 'status',
|
|
330
|
+
operator: 'IN' as const,
|
|
331
|
+
value: Array(10000).fill('test')
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
expect(() => validator.validate(hugeArray)).toThrow();
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe('Integration Security Tests', () => {
|
|
339
|
+
it('should maintain security through full parsing pipeline', () => {
|
|
340
|
+
const suspiciousQueries = [
|
|
341
|
+
'status:"active" OR 1=1',
|
|
342
|
+
'name:"admin\'--"',
|
|
343
|
+
'id:1 UNION SELECT password FROM users',
|
|
344
|
+
'field:"value"; INSERT INTO logs VALUES("hacked")'
|
|
345
|
+
];
|
|
346
|
+
|
|
347
|
+
suspiciousQueries.forEach(query => {
|
|
348
|
+
try {
|
|
349
|
+
const parsed = parser.parse(query);
|
|
350
|
+
validator.validate(parsed);
|
|
351
|
+
translator.translate(parsed);
|
|
352
|
+
|
|
353
|
+
// If no exception, ensure the translated query is safe
|
|
354
|
+
expect(true).toBe(true); // Placeholder for additional safety checks
|
|
355
|
+
} catch (error) {
|
|
356
|
+
// Should throw security or parse errors
|
|
357
|
+
expect(
|
|
358
|
+
error instanceof QueryParseError ||
|
|
359
|
+
error instanceof QuerySecurityError ||
|
|
360
|
+
error instanceof DrizzleTranslationError
|
|
361
|
+
).toBe(true);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { QueryParser } from '../../parser';
|
|
2
|
+
import { QueryExpression } from '../../parser/types';
|
|
3
|
+
import { DrizzleTranslator } from './';
|
|
4
|
+
import { SQL, sql } from 'drizzle-orm';
|
|
5
|
+
|
|
6
|
+
// Helper function to safely get SQL string value for testing
|
|
7
|
+
function getSqlString(sqlObj: SQL): string {
|
|
8
|
+
// For testing purposes only - extract a string representation
|
|
9
|
+
// of the SQL query that we can use in our assertions
|
|
10
|
+
try {
|
|
11
|
+
return JSON.stringify(sqlObj);
|
|
12
|
+
} catch (e) {
|
|
13
|
+
return String(sqlObj);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('DrizzleTranslator', () => {
|
|
18
|
+
const translator = new DrizzleTranslator();
|
|
19
|
+
const parser = new QueryParser();
|
|
20
|
+
|
|
21
|
+
describe('translate', () => {
|
|
22
|
+
it('should translate a simple comparison expression', () => {
|
|
23
|
+
const expression = parser.parse('priority:>2');
|
|
24
|
+
const result = translator.translate(expression);
|
|
25
|
+
|
|
26
|
+
const sqlString = getSqlString(result);
|
|
27
|
+
expect(sqlString).toContain('priority');
|
|
28
|
+
expect(sqlString).toContain('>');
|
|
29
|
+
expect(sqlString).toContain('2');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should translate a logical AND expression', () => {
|
|
33
|
+
const expression = parser.parse('priority:>2 AND status:"active"');
|
|
34
|
+
const result = translator.translate(expression);
|
|
35
|
+
|
|
36
|
+
const sqlString = getSqlString(result);
|
|
37
|
+
expect(sqlString).toContain('AND');
|
|
38
|
+
expect(sqlString).toContain('priority');
|
|
39
|
+
expect(sqlString).toContain('status');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should translate a logical OR expression', () => {
|
|
43
|
+
const expression = parser.parse('status:"active" OR status:"pending"');
|
|
44
|
+
const result = translator.translate(expression);
|
|
45
|
+
|
|
46
|
+
const sqlString = getSqlString(result);
|
|
47
|
+
expect(sqlString).toContain('OR');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should translate a NOT expression', () => {
|
|
51
|
+
const expression = parser.parse('NOT status:"inactive"');
|
|
52
|
+
const result = translator.translate(expression);
|
|
53
|
+
|
|
54
|
+
const sqlString = getSqlString(result);
|
|
55
|
+
expect(sqlString).toContain('NOT');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should translate multiple values as OR conditions', () => {
|
|
59
|
+
// Create a logical OR expression that checks for multiple values
|
|
60
|
+
const expression = parser.parse('status:"active" OR status:"pending"');
|
|
61
|
+
const result = translator.translate(expression);
|
|
62
|
+
|
|
63
|
+
const sqlString = getSqlString(result);
|
|
64
|
+
expect(sqlString).toContain('OR');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should translate a complex nested expression', () => {
|
|
68
|
+
const expression = parser.parse(
|
|
69
|
+
'(priority:>2 AND status:"active") OR (role:"admin")'
|
|
70
|
+
);
|
|
71
|
+
const result = translator.translate(expression);
|
|
72
|
+
|
|
73
|
+
const sqlString = getSqlString(result);
|
|
74
|
+
expect(sqlString).toContain('AND');
|
|
75
|
+
expect(sqlString).toContain('OR');
|
|
76
|
+
expect(sqlString).toContain('priority');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('with schema', () => {
|
|
81
|
+
// Mock schema with some table fields
|
|
82
|
+
const mockSchema = {
|
|
83
|
+
todos: {
|
|
84
|
+
id: sql.raw('todos.id') as unknown as SQL,
|
|
85
|
+
title: sql.raw('todos.title') as unknown as SQL,
|
|
86
|
+
priority: sql.raw('todos.priority') as unknown as SQL,
|
|
87
|
+
status: sql.raw('todos.status') as unknown as SQL
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const schemaTranslator = new DrizzleTranslator({
|
|
92
|
+
schema: mockSchema
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should use schema fields when available', () => {
|
|
96
|
+
const expression = parser.parse('todos.priority:>2');
|
|
97
|
+
const result = schemaTranslator.translate(expression);
|
|
98
|
+
|
|
99
|
+
const sqlString = getSqlString(result);
|
|
100
|
+
expect(sqlString).toContain('>');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('canTranslate', () => {
|
|
105
|
+
it('should return true for valid expressions', () => {
|
|
106
|
+
const expression = parser.parse('status:"active"');
|
|
107
|
+
expect(translator.canTranslate(expression)).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should handle unsupported expressions gracefully', () => {
|
|
111
|
+
// Create a malformed expression to test error handling
|
|
112
|
+
const invalidExpression = {
|
|
113
|
+
type: 'unsupported'
|
|
114
|
+
} as unknown as QueryExpression;
|
|
115
|
+
expect(translator.canTranslate(invalidExpression)).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('table qualification', () => {
|
|
120
|
+
it('should preserve table qualifiers in fields', () => {
|
|
121
|
+
const expression = parser.parse('todos.priority:>2');
|
|
122
|
+
const result = translator.translate(expression);
|
|
123
|
+
const sqlString = getSqlString(result);
|
|
124
|
+
|
|
125
|
+
expect(sqlString).toContain('todos.priority');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { DrizzleTranslator } from './index';
|
|
2
|
+
|
|
3
|
+
describe('DrizzleTranslator', () => {
|
|
4
|
+
let translator: DrizzleTranslator;
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
translator = new DrizzleTranslator();
|
|
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
|
+
});
|