qast 1.2.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 +260 -5
  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 +83 -8
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +108 -8
  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 +39 -0
  64. package/dist/parser/validator.d.ts.map +1 -1
  65. package/dist/parser/validator.js +144 -3
  66. package/dist/parser/validator.js.map +1 -1
  67. package/dist/types/ast.d.ts +59 -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 +29 -5
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`
@@ -268,13 +483,53 @@ Transform an AST to a TypeORM filter.
268
483
 
269
484
  **Note:** TypeORM requires operator functions for non-equality comparisons. You may need to transform the result.
270
485
 
271
- ### `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.
501
+
502
+ **Returns:** TypeORM filter object with `where`, `order`, `take`, and `skip` properties
503
+
504
+ **Note:** TypeORM requires operator functions for non-equality comparisons. Use `transformTypeORMOperators()` helper function.
505
+
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`
272
527
 
273
- Transform an AST to a Sequelize filter.
528
+ Transform an AST or QueryAST to a Drizzle filter.
274
529
 
275
- **Returns:** Sequelize filter object
530
+ **Returns:** Drizzle filter object with `where`, `orderBy`, `limit`, and `offset` properties
276
531
 
277
- **Note:** Sequelize uses the `Op` object. You need to transform `$`-prefixed operators to use `Op` operators.
532
+ **Note:** Use `transformDrizzleConditions()` helper function to convert to actual Drizzle conditions.
278
533
 
279
534
  ### `validateQuery(ast: QastNode, whitelist: WhitelistOptions): void`
280
535