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