qast 1.1.0 → 2.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 (83) hide show
  1. package/README.md +312 -6
  2. package/README.zh-CN.md +520 -0
  3. package/dist/adapters/drizzle.d.ts +66 -0
  4. package/dist/adapters/drizzle.d.ts.map +1 -0
  5. package/dist/adapters/drizzle.js +222 -0
  6. package/dist/adapters/drizzle.js.map +1 -0
  7. package/dist/adapters/knex.d.ts +22 -0
  8. package/dist/adapters/knex.d.ts.map +1 -0
  9. package/dist/adapters/knex.js +158 -0
  10. package/dist/adapters/knex.js.map +1 -0
  11. package/dist/adapters/mongoose.d.ts +15 -0
  12. package/dist/adapters/mongoose.d.ts.map +1 -0
  13. package/dist/adapters/mongoose.js +152 -0
  14. package/dist/adapters/mongoose.js.map +1 -0
  15. package/dist/adapters/prisma.d.ts +5 -2
  16. package/dist/adapters/prisma.d.ts.map +1 -1
  17. package/dist/adapters/prisma.js +103 -4
  18. package/dist/adapters/prisma.js.map +1 -1
  19. package/dist/adapters/sequelize.d.ts +26 -8
  20. package/dist/adapters/sequelize.d.ts.map +1 -1
  21. package/dist/adapters/sequelize.js +168 -7
  22. package/dist/adapters/sequelize.js.map +1 -1
  23. package/dist/adapters/typeorm.d.ts +32 -2
  24. package/dist/adapters/typeorm.d.ts.map +1 -1
  25. package/dist/adapters/typeorm.js +169 -3
  26. package/dist/adapters/typeorm.js.map +1 -1
  27. package/dist/builder/query-builder.d.ts +139 -0
  28. package/dist/builder/query-builder.d.ts.map +1 -0
  29. package/dist/builder/query-builder.js +320 -0
  30. package/dist/builder/query-builder.js.map +1 -0
  31. package/dist/errors.d.ts +18 -3
  32. package/dist/errors.d.ts.map +1 -1
  33. package/dist/errors.js +127 -9
  34. package/dist/errors.js.map +1 -1
  35. package/dist/index.d.ts +90 -10
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +142 -16
  38. package/dist/index.js.map +1 -1
  39. package/dist/integrations/express.d.ts +14 -0
  40. package/dist/integrations/express.d.ts.map +1 -0
  41. package/dist/integrations/express.js +33 -0
  42. package/dist/integrations/express.js.map +1 -0
  43. package/dist/integrations/fastify.d.ts +17 -0
  44. package/dist/integrations/fastify.d.ts.map +1 -0
  45. package/dist/integrations/fastify.js +35 -0
  46. package/dist/integrations/fastify.js.map +1 -0
  47. package/dist/integrations/hono.d.ts +14 -0
  48. package/dist/integrations/hono.d.ts.map +1 -0
  49. package/dist/integrations/hono.js +30 -0
  50. package/dist/integrations/hono.js.map +1 -0
  51. package/dist/integrations/nestjs.d.ts +23 -0
  52. package/dist/integrations/nestjs.d.ts.map +1 -0
  53. package/dist/integrations/nestjs.js +137 -0
  54. package/dist/integrations/nestjs.js.map +1 -0
  55. package/dist/parser/parser.d.ts +15 -7
  56. package/dist/parser/parser.d.ts.map +1 -1
  57. package/dist/parser/parser.js +117 -15
  58. package/dist/parser/parser.js.map +1 -1
  59. package/dist/parser/tokenizer.d.ts +11 -1
  60. package/dist/parser/tokenizer.d.ts.map +1 -1
  61. package/dist/parser/tokenizer.js +67 -10
  62. package/dist/parser/tokenizer.js.map +1 -1
  63. package/dist/parser/validator.d.ts +44 -1
  64. package/dist/parser/validator.d.ts.map +1 -1
  65. package/dist/parser/validator.js +210 -2
  66. package/dist/parser/validator.js.map +1 -1
  67. package/dist/types/ast.d.ts +102 -3
  68. package/dist/types/ast.d.ts.map +1 -1
  69. package/dist/types/ast.js +21 -0
  70. package/dist/types/ast.js.map +1 -1
  71. package/dist/utils/ast-utils.d.ts +43 -0
  72. package/dist/utils/ast-utils.d.ts.map +1 -0
  73. package/dist/utils/ast-utils.js +205 -0
  74. package/dist/utils/ast-utils.js.map +1 -0
  75. package/dist/utils/cache.d.ts +47 -0
  76. package/dist/utils/cache.d.ts.map +1 -0
  77. package/dist/utils/cache.js +132 -0
  78. package/dist/utils/cache.js.map +1 -0
  79. package/dist/utils/serializer.d.ts +6 -0
  80. package/dist/utils/serializer.d.ts.map +1 -0
  81. package/dist/utils/serializer.js +140 -0
  82. package/dist/utils/serializer.js.map +1 -0
  83. package/package.json +31 -8
package/README.md CHANGED
@@ -70,19 +70,35 @@ QAST supports the following comparison operators:
70
70
  - `gte` - Greater than or equal
71
71
  - `lte` - Less than or equal
72
72
  - `in` - In array
73
+ - `notIn` - Not in array
73
74
  - `contains` - Contains substring (string matching)
75
+ - `startsWith` - String starts with pattern
76
+ - `endsWith` - String ends with pattern
77
+ - `like` - Pattern matching with wildcards (`%` and `_`)
78
+ - `regex` / `matches` - Regex pattern matching
79
+ - `between` - Range check: `age between 18 and 65`
80
+ - `isNull` - Null check: `email isNull`
81
+ - `isNotNull` - Not null check: `email isNotNull`
74
82
 
75
83
  ### Logical Operators
76
84
 
77
85
  - `and` - Logical AND
78
86
  - `or` - Logical OR
87
+ - `not` - Logical NOT: `not (age gt 25)`
88
+
89
+ ### Query Clauses
90
+
91
+ - **Sorting**: `orderBy age desc, name asc` - Sort by multiple fields
92
+ - **Pagination**: `limit 10 offset 20` - Limit and offset results
93
+ - **Nested fields**: `user.profile.name eq "John"` - Access nested fields with dot notation
79
94
 
80
95
  ### Values
81
96
 
82
97
  - **Strings**: Use single or double quotes: `"John"` or `'John'`
83
98
  - **Numbers**: Integers or floats: `25`, `25.99`, `-10`
84
99
  - **Booleans**: `true` or `false`
85
- - **Arrays**: For `in` operator: `[1,2,3]` or `["John","Jane"]`
100
+ - **Null**: `null` - For null checks
101
+ - **Arrays**: For `in` and `notIn` operators: `[1,2,3]` or `["John","Jane"]`
86
102
 
87
103
  ### Examples
88
104
 
@@ -110,6 +126,19 @@ QAST supports the following comparison operators:
110
126
 
111
127
  // Complex query
112
128
  'age gt 25 and (name eq "John" or city eq "Paris") and active eq true'
129
+
130
+ // New operators
131
+ 'age between 18 and 65'
132
+ 'age notIn [1,2,3]'
133
+ 'name like "John%"'
134
+ 'email matches "^[a-z]+@example\.com$"'
135
+ 'email isNull'
136
+ 'email isNotNull'
137
+ 'not (age gt 25)'
138
+
139
+ // Sorting and pagination
140
+ 'age gt 25 orderBy age desc limit 10 offset 20'
141
+ 'name eq "John" orderBy name asc, age desc limit 5'
113
142
  ```
114
143
 
115
144
  ## ORM Adapters
@@ -220,6 +249,192 @@ await User.findAll({ where: transformed });
220
249
 
221
250
  **Note**: Sequelize uses the `Op` object from 'sequelize'. Since Sequelize is an optional peer dependency, the adapter returns a structure with metadata (`__qast_operator__` and `__qast_logical__`) that you need to transform to use `Op` operators. For simple equality (`eq`), the adapter returns plain values which Sequelize accepts directly.
222
251
 
252
+ ### Mongoose
253
+
254
+ ```typescript
255
+ import { parseQueryFull, toMongooseFilter } from 'qast';
256
+
257
+ const query = 'age gt 25 orderBy age desc limit 10';
258
+ const queryAST = parseQueryFull(query);
259
+ const filter = toMongooseFilter(queryAST);
260
+
261
+ // filter = {
262
+ // filter: { age: { $gt: 25 } },
263
+ // sort: { age: -1 },
264
+ // limit: 10
265
+ // }
266
+
267
+ await User.find(filter.filter)
268
+ .sort(filter.sort)
269
+ .limit(filter.limit);
270
+ ```
271
+
272
+ ### Knex.js
273
+
274
+ ```typescript
275
+ import { parseQueryFull, toKnexFilter } from 'qast';
276
+
277
+ const query = 'age gt 25 orderBy age desc limit 10';
278
+ const queryAST = parseQueryFull(query);
279
+ const filter = toKnexFilter(queryAST);
280
+
281
+ // Use with Knex query builder
282
+ knex('users')
283
+ .where(filter.whereCallback)
284
+ .orderBy(filter.orderBy!)
285
+ .limit(filter.limit!)
286
+ .offset(filter.offset);
287
+ ```
288
+
289
+ ### Drizzle ORM
290
+
291
+ ```typescript
292
+ import { parseQueryFull, toDrizzleFilter, transformDrizzleConditions } from 'qast';
293
+ import { and, gt, eq, desc } from 'drizzle-orm';
294
+
295
+ const query = 'age gt 25 and name eq "John"';
296
+ const queryAST = parseQueryFull(query);
297
+ const filter = toDrizzleFilter(queryAST);
298
+
299
+ // Transform to Drizzle conditions
300
+ const columnMap = { age: users.age, name: users.name };
301
+ const where = transformDrizzleConditions(filter.where, { and, gt, eq }, columnMap);
302
+
303
+ await db.select().from(users).where(where);
304
+ ```
305
+
306
+ ## Query Builder API
307
+
308
+ Build queries programmatically using a fluent API:
309
+
310
+ ```typescript
311
+ import { queryBuilder } from 'qast';
312
+
313
+ const query = queryBuilder()
314
+ .field('age').gt(25)
315
+ .and(queryBuilder().field('name').eq('John'))
316
+ .orderBy('age', 'desc')
317
+ .limit(10)
318
+ .offset(20)
319
+ .build();
320
+
321
+ // query.filter - the filter AST
322
+ // query.orderBy - sorting specifications
323
+ // query.limit - limit value
324
+ // query.offset - offset value
325
+ ```
326
+
327
+ ## AST Serialization
328
+
329
+ Convert AST back to query string:
330
+
331
+ ```typescript
332
+ import { parseQuery, serializeQuery } from 'qast';
333
+
334
+ const query = 'age gt 25 and name eq "John"';
335
+ const ast = parseQuery(query);
336
+ const serialized = serializeQuery(ast);
337
+ // serialized = 'age gt 25 and name eq "John"'
338
+ ```
339
+
340
+ ## AST Utilities
341
+
342
+ ### Clone AST
343
+
344
+ ```typescript
345
+ import { parseQuery, cloneAST } from 'qast';
346
+
347
+ const ast = parseQuery('age gt 25');
348
+ const cloned = cloneAST(ast);
349
+ ```
350
+
351
+ ### Merge Queries
352
+
353
+ ```typescript
354
+ import { parseQuery, mergeQueries } from 'qast';
355
+
356
+ const query1 = parseQuery('age gt 25');
357
+ const query2 = parseQuery('name eq "John"');
358
+ const merged = mergeQueries(query1, query2, 'AND');
359
+ ```
360
+
361
+ ### Get Query Statistics
362
+
363
+ ```typescript
364
+ import { parseQuery, getQueryStats } from 'qast';
365
+
366
+ const ast = parseQuery('age gt 25 and name eq "John"');
367
+ const stats = getQueryStats(ast);
368
+ // stats.depth, stats.nodeCount, stats.fields, etc.
369
+ ```
370
+
371
+ ### Optimize AST
372
+
373
+ ```typescript
374
+ import { parseQuery, optimizeAST } from 'qast';
375
+
376
+ const ast = parseQuery('age gt 25 and age gt 25'); // Redundant
377
+ const optimized = optimizeAST(ast);
378
+ ```
379
+
380
+ ### Compare Queries
381
+
382
+ ```typescript
383
+ import { parseQuery, diffQueries } from 'qast';
384
+
385
+ const query1 = parseQuery('age gt 25');
386
+ const query2 = parseQuery('age gt 30');
387
+ const diff = diffQueries(query1, query2);
388
+ ```
389
+
390
+ ## Performance Features
391
+
392
+ ### Query Caching
393
+
394
+ ```typescript
395
+ import { QueryCache, createCachedParser, parseQuery } from 'qast';
396
+
397
+ // Create a cached parser
398
+ const cachedParse = createCachedParser(parseQuery, 100, 60000); // 100 items, 60s TTL
399
+
400
+ // Use cached parser
401
+ const ast = cachedParse('age gt 25'); // Cached on subsequent calls
402
+ ```
403
+
404
+ ## Security Features
405
+
406
+ ### Field Sanitization
407
+
408
+ ```typescript
409
+ import { sanitizeFieldName } from 'qast';
410
+
411
+ const safeField = sanitizeFieldName('user.name'); // Removes dangerous characters
412
+ ```
413
+
414
+ ### Query Cost Estimation
415
+
416
+ ```typescript
417
+ import { parseQuery, estimateQueryCost } from 'qast';
418
+
419
+ const ast = parseQuery('age gt 25 and name eq "John"');
420
+ const cost = estimateQueryCost(ast);
421
+ // cost.estimatedComplexity, cost.depth, cost.nodeCount, etc.
422
+ ```
423
+
424
+ ### Rate Limiting
425
+
426
+ ```typescript
427
+ import { RateLimiter } from 'qast';
428
+
429
+ const limiter = new RateLimiter(100, 60000); // 100 requests per 60 seconds
430
+
431
+ if (limiter.isAllowed(userId)) {
432
+ // Process query
433
+ } else {
434
+ // Rate limit exceeded
435
+ }
436
+ ```
437
+
223
438
  ## API Reference
224
439
 
225
440
  ### `parseQuery(query: string, options?: ParseOptions): QastNode`
@@ -232,6 +447,11 @@ Parse a query string into an AST.
232
447
  - `allowedFields?: string[]` - Whitelist of allowed field names
233
448
  - `allowedOperators?: Operator[]` - Whitelist of allowed operators
234
449
  - `validate?: boolean` - Whether to validate against whitelists (default: true if whitelists are provided)
450
+ - `maxDepth?: number` - Maximum allowed AST depth (to limit nested logical expressions)
451
+ - `maxNodes?: number` - Maximum allowed number of AST nodes (to limit overall query complexity)
452
+ - `maxQueryLength?: number` - Maximum allowed length of the raw query string (checked before parsing)
453
+ - `maxArrayLength?: number` - Maximum allowed length of array values (for `in` operator)
454
+ - `maxStringLength?: number` - Maximum allowed length of string values
235
455
 
236
456
  **Returns:** The parsed AST node
237
457
 
@@ -241,6 +461,11 @@ const ast = parseQuery('age gt 25', {
241
461
  allowedFields: ['age', 'name'],
242
462
  allowedOperators: ['gt', 'eq'],
243
463
  validate: true,
464
+ maxDepth: 5,
465
+ maxNodes: 50,
466
+ maxQueryLength: 1000,
467
+ maxArrayLength: 100,
468
+ maxStringLength: 200,
244
469
  });
245
470
  ```
246
471
 
@@ -258,13 +483,53 @@ Transform an AST to a TypeORM filter.
258
483
 
259
484
  **Note:** TypeORM requires operator functions for non-equality comparisons. You may need to transform the result.
260
485
 
261
- ### `toSequelizeFilter(ast: QastNode): SequelizeFilter`
486
+ ### `parseQueryFull(query: string, options?: ParseOptions): QueryAST`
487
+
488
+ Parse a query string into a full QueryAST (includes filter, sorting, pagination).
489
+
490
+ **Returns:** QueryAST with filter, orderBy, limit, and offset
491
+
492
+ ### `toPrismaFilter(ast: QastNode | QueryAST): PrismaFilter`
493
+
494
+ Transform an AST or QueryAST to a Prisma filter.
495
+
496
+ **Returns:** Prisma filter object with `where`, `orderBy`, `take`, and `skip` properties
497
+
498
+ ### `toTypeORMFilter(ast: QastNode | QueryAST): TypeORMFilter`
499
+
500
+ Transform an AST or QueryAST to a TypeORM filter.
262
501
 
263
- Transform an AST to a Sequelize filter.
502
+ **Returns:** TypeORM filter object with `where`, `order`, `take`, and `skip` properties
264
503
 
265
- **Returns:** Sequelize filter object
504
+ **Note:** TypeORM requires operator functions for non-equality comparisons. Use `transformTypeORMOperators()` helper function.
266
505
 
267
- **Note:** Sequelize uses the `Op` object. You need to transform `$`-prefixed operators to use `Op` operators.
506
+ ### `toSequelizeFilter(ast: QastNode | QueryAST): SequelizeFilter`
507
+
508
+ Transform an AST or QueryAST to a Sequelize filter.
509
+
510
+ **Returns:** Sequelize filter object with `where`, `order`, `limit`, and `offset` properties
511
+
512
+ **Note:** Sequelize uses the `Op` object. Use `transformSequelizeOperators()` helper function.
513
+
514
+ ### `toMongooseFilter(ast: QastNode | QueryAST): MongooseFilter`
515
+
516
+ Transform an AST or QueryAST to a Mongoose filter.
517
+
518
+ **Returns:** Mongoose filter object with `filter`, `sort`, `limit`, and `skip` properties
519
+
520
+ ### `toKnexFilter(ast: QastNode | QueryAST): KnexFilter`
521
+
522
+ Transform an AST or QueryAST to a Knex filter.
523
+
524
+ **Returns:** Knex filter object with `whereCallback`, `orderBy`, `limit`, and `offset` properties
525
+
526
+ ### `toDrizzleFilter(ast: QastNode | QueryAST): DrizzleFilter`
527
+
528
+ Transform an AST or QueryAST to a Drizzle filter.
529
+
530
+ **Returns:** Drizzle filter object with `where`, `orderBy`, `limit`, and `offset` properties
531
+
532
+ **Note:** Use `transformDrizzleConditions()` helper function to convert to actual Drizzle conditions.
268
533
 
269
534
  ### `validateQuery(ast: QastNode, whitelist: WhitelistOptions): void`
270
535
 
@@ -290,6 +555,33 @@ Extract all operators used in an AST.
290
555
 
291
556
  **Returns:** Array of unique operators
292
557
 
558
+ ### `validateQueryComplexity(ast: QastNode, options: ComplexityOptions): void`
559
+
560
+ Validate an AST against complexity limits.
561
+
562
+ **Parameters:**
563
+ - `ast` - The AST to validate
564
+ - `options` - Complexity options:
565
+ - `maxDepth?: number` - Maximum allowed AST depth
566
+ - `maxNodes?: number` - Maximum allowed number of nodes
567
+ - `maxArrayLength?: number` - Maximum allowed array length
568
+ - `maxStringLength?: number` - Maximum allowed string length
569
+
570
+ **Throws:** `ValidationError` if any limit is exceeded
571
+
572
+ **Example:**
573
+ ```typescript
574
+ import { parseQuery, validateQueryComplexity } from 'qast';
575
+
576
+ const ast = parseQuery('age in [1,2,3,4,5]');
577
+ validateQueryComplexity(ast, {
578
+ maxDepth: 5,
579
+ maxNodes: 20,
580
+ maxArrayLength: 100,
581
+ maxStringLength: 200,
582
+ });
583
+ ```
584
+
293
585
  ## Security Best Practices
294
586
 
295
587
  1. **Always use whitelists**: Restrict which fields and operators can be used in queries.
@@ -304,7 +596,21 @@ const ast = parseQuery(req.query.filter, {
304
596
 
305
597
  2. **Validate user input**: Don't trust user-provided query strings without validation.
306
598
 
307
- 3. **Limit query complexity**: Consider limiting the depth of nested queries to prevent DoS attacks.
599
+ 3. **Limit query complexity**: Use complexity limits to prevent DoS attacks.
600
+
601
+ ```typescript
602
+ const ast = parseQuery(req.query.filter, {
603
+ allowedFields: ['age', 'name', 'city'],
604
+ allowedOperators: ['gt', 'eq', 'lt', 'in'],
605
+ validate: true,
606
+ // Complexity limits
607
+ maxQueryLength: 1000, // Reject queries longer than 1000 chars
608
+ maxDepth: 5, // Max 5 levels of nesting
609
+ maxNodes: 20, // Max 20 conditions
610
+ maxArrayLength: 100, // Max 100 items in 'in' arrays
611
+ maxStringLength: 200, // Max 200 chars per string value
612
+ });
613
+ ```
308
614
 
309
615
  4. **Use type checking**: Ensure values match expected types for fields.
310
616