@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,395 @@
1
+ import { IComparisonExpression, QueryExpression } from '../parser/types';
2
+ import { DEFAULT_SECURITY_OPTIONS, ISecurityOptions } from './types';
3
+
4
+ /**
5
+ * Error thrown when a query violates security constraints
6
+ *
7
+ * This error is thrown when a query attempts to bypass security settings
8
+ * such as accessing unauthorized fields, exceeding complexity limits,
9
+ * or using potentially dangerous patterns.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * try {
14
+ * queryValidator.validate(parsedQuery);
15
+ * } catch (error) {
16
+ * if (error instanceof QuerySecurityError) {
17
+ * console.error('Security violation:', error.message);
18
+ * // Return appropriate error response to client
19
+ * }
20
+ * }
21
+ * ```
22
+ */
23
+ export class QuerySecurityError extends Error {
24
+ constructor(message: string) {
25
+ super(message);
26
+ this.name = 'QuerySecurityError';
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Validates query expressions against security constraints
32
+ *
33
+ * The QuerySecurityValidator ensures that queries comply with the security
34
+ * rules defined in the ISecurityOptions. It should be used before executing
35
+ * any query to prevent potential security issues or resource exhaustion.
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * import { QuerySecurityValidator, ISecurityOptions } from 'querykit';
40
+ * import { parseQuery } from 'querykit/parser';
41
+ *
42
+ * // Define security options
43
+ * const securityOptions: ISecurityOptions = {
44
+ * allowedFields: ['id', 'name', 'createdAt'],
45
+ * denyFields: ['password'],
46
+ * maxQueryDepth: 5
47
+ * };
48
+ *
49
+ * // Create validator
50
+ * const validator = new QuerySecurityValidator(securityOptions);
51
+ *
52
+ * // Use in API endpoint
53
+ * app.get('/users', (req, res) => {
54
+ * try {
55
+ * // Parse the query from request
56
+ * const queryStr = req.query.filter || '';
57
+ * const parsedQuery = parseQuery(queryStr);
58
+ *
59
+ * // Validate against security rules
60
+ * validator.validate(parsedQuery, userSchema);
61
+ *
62
+ * // If validation passes, execute the query
63
+ * const results = executeQuery(parsedQuery);
64
+ * res.json(results);
65
+ * } catch (error) {
66
+ * if (error instanceof QuerySecurityError) {
67
+ * res.status(400).json({ error: error.message });
68
+ * } else {
69
+ * res.status(500).json({ error: 'Server error' });
70
+ * }
71
+ * }
72
+ * });
73
+ * ```
74
+ */
75
+ export class QuerySecurityValidator {
76
+ private options: Required<ISecurityOptions>;
77
+
78
+ /**
79
+ * Creates a new QuerySecurityValidator instance
80
+ *
81
+ * @param options - Security options to apply. If not provided, default options will be used.
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * // Create with default security settings
86
+ * const defaultValidator = new QuerySecurityValidator();
87
+ *
88
+ * // Create with custom security settings
89
+ * const strictValidator = new QuerySecurityValidator({
90
+ * maxQueryDepth: 3,
91
+ * maxClauseCount: 10,
92
+ * maxValueLength: 500
93
+ * });
94
+ * ```
95
+ */
96
+ constructor(options: ISecurityOptions = {}) {
97
+ this.options = {
98
+ ...DEFAULT_SECURITY_OPTIONS,
99
+ ...options
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Validate a query expression against security constraints
105
+ *
106
+ * This is the main method that should be called before executing any query.
107
+ * It performs all security checks defined in the options.
108
+ *
109
+ * @param expression - The parsed query expression to validate
110
+ * @param schema - Optional schema definition to validate fields against
111
+ * @throws {QuerySecurityError} If the query violates any security constraints
112
+ *
113
+ * @example
114
+ * ```typescript
115
+ * import { parseQuery } from 'querykit/parser';
116
+ *
117
+ * const validator = new QuerySecurityValidator();
118
+ *
119
+ * // Simple validation
120
+ * try {
121
+ * const query = parseQuery('user.name == "John" && user.priority > 2');
122
+ * validator.validate(query);
123
+ * // Query is safe to execute
124
+ * } catch (error) {
125
+ * // Handle security violation
126
+ * }
127
+ *
128
+ * // Validation with schema
129
+ * const userSchema = {
130
+ * user: {
131
+ * id: 'number',
132
+ * name: 'string',
133
+ * priority: 'number',
134
+ * email: 'string'
135
+ * }
136
+ * };
137
+ *
138
+ * try {
139
+ * const query = parseQuery('user.email == "john@example.com"');
140
+ * validator.validate(query, userSchema);
141
+ * // Query is safe to execute
142
+ * } catch (error) {
143
+ * // Handle security violation
144
+ * }
145
+ * ```
146
+ */
147
+ public validate(
148
+ expression: QueryExpression,
149
+ schema?: Record<string, Record<string, unknown>>
150
+ ): void {
151
+ // Check for field restrictions if specified
152
+ this.validateFields(expression, schema);
153
+
154
+ // Check query complexity
155
+ this.validateQueryDepth(expression, 0);
156
+ this.validateClauseCount(expression);
157
+
158
+ // Check value lengths
159
+ this.validateValueLengths(expression);
160
+
161
+ // Sanitize wildcard patterns if enabled
162
+ if (this.options.sanitizeWildcards) {
163
+ this.sanitizeWildcards(expression);
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Validate that query fields are allowed and not denied
169
+ *
170
+ * @private
171
+ * @param expression - The query expression to validate
172
+ * @param schema - Optional schema definition to validate fields against
173
+ */
174
+ private validateFields(
175
+ expression: QueryExpression,
176
+ schema?: Record<string, Record<string, unknown>>
177
+ ): void {
178
+ const fieldSet = new Set<string>();
179
+ this.collectFields(expression, fieldSet);
180
+
181
+ // Create a set of allowed fields
182
+ const allowedFields = new Set<string>();
183
+
184
+ // If allowedFields is empty and schema is provided, use schema fields
185
+ if (this.options.allowedFields.length === 0 && schema) {
186
+ for (const table in schema) {
187
+ if (typeof schema[table] === 'object') {
188
+ for (const field in schema[table]) {
189
+ allowedFields.add(`${table}.${field}`);
190
+ allowedFields.add(field);
191
+ }
192
+ }
193
+ }
194
+ } else {
195
+ this.options.allowedFields.forEach(field => allowedFields.add(field));
196
+ }
197
+
198
+ // Create a set of denied fields
199
+ const deniedFields = new Set<string>(this.options.denyFields);
200
+
201
+ // Check each field in the query
202
+ for (const field of fieldSet) {
203
+ // Security fix: Generic error to prevent field enumeration attacks
204
+ if (
205
+ deniedFields.has(field) ||
206
+ (allowedFields.size > 0 && !allowedFields.has(field))
207
+ ) {
208
+ throw new QuerySecurityError('Invalid query parameters');
209
+ }
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Validate that query depth does not exceed the maximum
215
+ *
216
+ * @private
217
+ * @param expression - The query expression to validate
218
+ * @param currentDepth - The current depth level in the recursion
219
+ */
220
+ private validateQueryDepth(
221
+ expression: QueryExpression,
222
+ currentDepth: number
223
+ ): void {
224
+ if (currentDepth > this.options.maxQueryDepth) {
225
+ throw new QuerySecurityError(
226
+ `Query exceeds maximum depth of ${this.options.maxQueryDepth}`
227
+ );
228
+ }
229
+
230
+ if (expression.type === 'logical') {
231
+ this.validateQueryDepth(expression.left, currentDepth + 1);
232
+ if (expression.right) {
233
+ this.validateQueryDepth(expression.right, currentDepth + 1);
234
+ }
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Validate that the number of clauses does not exceed the maximum
240
+ *
241
+ * @private
242
+ * @param expression - The query expression to validate
243
+ */
244
+ private validateClauseCount(expression: QueryExpression): void {
245
+ const count = this.countClauses(expression);
246
+ if (count > this.options.maxClauseCount) {
247
+ throw new QuerySecurityError(
248
+ `Query exceeds maximum clause count of ${this.options.maxClauseCount} (found ${count})`
249
+ );
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Count the number of clauses in a query expression
255
+ *
256
+ * @private
257
+ * @param expression - The query expression to count clauses in
258
+ * @returns The total number of comparison clauses
259
+ */
260
+ private countClauses(expression: QueryExpression): number {
261
+ if (expression.type === 'comparison') {
262
+ return 1;
263
+ }
264
+
265
+ let count = 0;
266
+ count += this.countClauses(expression.left);
267
+ if (expression.right) {
268
+ count += this.countClauses(expression.right);
269
+ }
270
+ return count;
271
+ }
272
+
273
+ /**
274
+ * Validate that string values do not exceed maximum length
275
+ * Security: Enhanced to prevent type confusion attacks via arrays/objects
276
+ *
277
+ * @private
278
+ * @param expression - The query expression to validate
279
+ */
280
+ private validateValueLengths(expression: QueryExpression): void {
281
+ if (expression.type === 'comparison') {
282
+ const { value } = expression;
283
+
284
+ // Check string values
285
+ if (
286
+ typeof value === 'string' &&
287
+ value.length > this.options.maxValueLength
288
+ ) {
289
+ throw new QuerySecurityError(
290
+ `Query contains a string value that exceeds maximum length of ${this.options.maxValueLength} characters`
291
+ );
292
+ }
293
+
294
+ // Security fix: Enhanced array validation to prevent bypass
295
+ if (Array.isArray(value)) {
296
+ if (value.length > 100) {
297
+ // Limit array size
298
+ throw new QuerySecurityError('Array values cannot exceed 100 items');
299
+ }
300
+
301
+ for (const item of value) {
302
+ if (
303
+ typeof item === 'string' &&
304
+ item.length > this.options.maxValueLength
305
+ ) {
306
+ throw new QuerySecurityError(
307
+ `Query contains a string value in array that exceeds maximum length of ${this.options.maxValueLength} characters`
308
+ );
309
+ }
310
+
311
+ // Security fix: Prevent object injection in arrays
312
+ if (typeof item === 'object' && item !== null) {
313
+ throw new QuerySecurityError(
314
+ 'Object values are not allowed in arrays'
315
+ );
316
+ }
317
+ }
318
+ }
319
+
320
+ // Security fix: Prevent object values entirely
321
+ if (
322
+ typeof value === 'object' &&
323
+ value !== null &&
324
+ !Array.isArray(value)
325
+ ) {
326
+ throw new QuerySecurityError('Object values are not allowed');
327
+ }
328
+ } else {
329
+ this.validateValueLengths(expression.left);
330
+ if (expression.right) {
331
+ this.validateValueLengths(expression.right);
332
+ }
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Sanitize wildcard patterns in LIKE queries to prevent regex DoS
338
+ * Security: Enhanced to prevent ReDoS attacks via catastrophic backtracking
339
+ *
340
+ * @private
341
+ * @param expression - The query expression to sanitize
342
+ */
343
+ private sanitizeWildcards(expression: QueryExpression): void {
344
+ if (expression.type === 'comparison') {
345
+ const { operator, value } = expression;
346
+
347
+ // Only sanitize LIKE operators with string values
348
+ if (operator === 'LIKE' && typeof value === 'string') {
349
+ // Security fix: Count wildcards to prevent ReDoS
350
+ const wildcardCount = (value.match(/[*?]/g) || []).length;
351
+ if (wildcardCount > 10) {
352
+ throw new QuerySecurityError('Excessive wildcard usage');
353
+ }
354
+
355
+ // Security fix: Prevent alternating patterns that cause catastrophic backtracking
356
+ // Pattern like "*a*b*c*d*e*f" (alternating * and non-* chars)
357
+ if (/(\*[^*]+){5,}/.test(value)) {
358
+ throw new QuerySecurityError('Complex wildcard patterns not allowed');
359
+ }
360
+
361
+ // Enhanced sanitization: limit consecutive wildcards
362
+ const sanitized = value
363
+ .replace(/\*{2,}/g, '*') // Limit consecutive asterisks
364
+ .replace(/\?{2,}/g, '?'); // Limit consecutive question marks
365
+ (expression as IComparisonExpression).value = sanitized;
366
+ }
367
+ } else {
368
+ this.sanitizeWildcards(expression.left);
369
+ if (expression.right) {
370
+ this.sanitizeWildcards(expression.right);
371
+ }
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Collect all field names used in the query
377
+ *
378
+ * @private
379
+ * @param expression - The query expression to collect fields from
380
+ * @param fieldSet - Set to store the collected field names
381
+ */
382
+ private collectFields(
383
+ expression: QueryExpression,
384
+ fieldSet: Set<string>
385
+ ): void {
386
+ if (expression.type === 'comparison') {
387
+ fieldSet.add(expression.field);
388
+ } else {
389
+ this.collectFields(expression.left, fieldSet);
390
+ if (expression.right) {
391
+ this.collectFields(expression.right, fieldSet);
392
+ }
393
+ }
394
+ }
395
+ }