qast 1.2.0 → 2.0.2

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 (84) hide show
  1. package/LICENSE +22 -22
  2. package/README.md +775 -520
  3. package/README.zh-CN.md +520 -0
  4. package/dist/adapters/drizzle.d.ts +66 -0
  5. package/dist/adapters/drizzle.d.ts.map +1 -0
  6. package/dist/adapters/drizzle.js +222 -0
  7. package/dist/adapters/drizzle.js.map +1 -0
  8. package/dist/adapters/knex.d.ts +22 -0
  9. package/dist/adapters/knex.d.ts.map +1 -0
  10. package/dist/adapters/knex.js +158 -0
  11. package/dist/adapters/knex.js.map +1 -0
  12. package/dist/adapters/mongoose.d.ts +15 -0
  13. package/dist/adapters/mongoose.d.ts.map +1 -0
  14. package/dist/adapters/mongoose.js +153 -0
  15. package/dist/adapters/mongoose.js.map +1 -0
  16. package/dist/adapters/prisma.d.ts +5 -2
  17. package/dist/adapters/prisma.d.ts.map +1 -1
  18. package/dist/adapters/prisma.js +102 -4
  19. package/dist/adapters/prisma.js.map +1 -1
  20. package/dist/adapters/sequelize.d.ts +26 -8
  21. package/dist/adapters/sequelize.d.ts.map +1 -1
  22. package/dist/adapters/sequelize.js +168 -7
  23. package/dist/adapters/sequelize.js.map +1 -1
  24. package/dist/adapters/typeorm.d.ts +32 -2
  25. package/dist/adapters/typeorm.d.ts.map +1 -1
  26. package/dist/adapters/typeorm.js +169 -3
  27. package/dist/adapters/typeorm.js.map +1 -1
  28. package/dist/builder/query-builder.d.ts +139 -0
  29. package/dist/builder/query-builder.d.ts.map +1 -0
  30. package/dist/builder/query-builder.js +320 -0
  31. package/dist/builder/query-builder.js.map +1 -0
  32. package/dist/errors.d.ts +18 -3
  33. package/dist/errors.d.ts.map +1 -1
  34. package/dist/errors.js +130 -10
  35. package/dist/errors.js.map +1 -1
  36. package/dist/index.d.ts +77 -11
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +124 -16
  39. package/dist/index.js.map +1 -1
  40. package/dist/integrations/express.d.ts +14 -0
  41. package/dist/integrations/express.d.ts.map +1 -0
  42. package/dist/integrations/express.js +33 -0
  43. package/dist/integrations/express.js.map +1 -0
  44. package/dist/integrations/fastify.d.ts +17 -0
  45. package/dist/integrations/fastify.d.ts.map +1 -0
  46. package/dist/integrations/fastify.js +35 -0
  47. package/dist/integrations/fastify.js.map +1 -0
  48. package/dist/integrations/hono.d.ts +14 -0
  49. package/dist/integrations/hono.d.ts.map +1 -0
  50. package/dist/integrations/hono.js +30 -0
  51. package/dist/integrations/hono.js.map +1 -0
  52. package/dist/integrations/nestjs.d.ts +23 -0
  53. package/dist/integrations/nestjs.d.ts.map +1 -0
  54. package/dist/integrations/nestjs.js +137 -0
  55. package/dist/integrations/nestjs.js.map +1 -0
  56. package/dist/parser/parser.d.ts +15 -7
  57. package/dist/parser/parser.d.ts.map +1 -1
  58. package/dist/parser/parser.js +117 -15
  59. package/dist/parser/parser.js.map +1 -1
  60. package/dist/parser/tokenizer.d.ts +11 -1
  61. package/dist/parser/tokenizer.d.ts.map +1 -1
  62. package/dist/parser/tokenizer.js +67 -10
  63. package/dist/parser/tokenizer.js.map +1 -1
  64. package/dist/parser/validator.d.ts +39 -0
  65. package/dist/parser/validator.d.ts.map +1 -1
  66. package/dist/parser/validator.js +144 -3
  67. package/dist/parser/validator.js.map +1 -1
  68. package/dist/types/ast.d.ts +59 -3
  69. package/dist/types/ast.d.ts.map +1 -1
  70. package/dist/types/ast.js +21 -0
  71. package/dist/types/ast.js.map +1 -1
  72. package/dist/utils/ast-utils.d.ts +43 -0
  73. package/dist/utils/ast-utils.d.ts.map +1 -0
  74. package/dist/utils/ast-utils.js +205 -0
  75. package/dist/utils/ast-utils.js.map +1 -0
  76. package/dist/utils/cache.d.ts +47 -0
  77. package/dist/utils/cache.d.ts.map +1 -0
  78. package/dist/utils/cache.js +132 -0
  79. package/dist/utils/cache.js.map +1 -0
  80. package/dist/utils/serializer.d.ts +6 -0
  81. package/dist/utils/serializer.d.ts.map +1 -0
  82. package/dist/utils/serializer.js +140 -0
  83. package/dist/utils/serializer.js.map +1 -0
  84. package/package.json +124 -84
package/README.md CHANGED
@@ -1,520 +1,775 @@
1
- # QAST — Query to AST to ORM
2
-
3
- [![npm version](https://img.shields.io/npm/v/qast.svg)](https://www.npmjs.com/package/qast)
4
- [![npm downloads](https://img.shields.io/npm/dm/qast.svg)](https://www.npmjs.com/package/qast)
5
- [![GitHub stars](https://img.shields.io/github/stars/hocestnonsatis/qast.svg)](https://github.com/hocestnonsatis/qast)
6
- [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/)
7
- [![License: MIT](https://img.shields.io/npm/l/qast.svg)](https://github.com/hocestnonsatis/qast/blob/main/LICENSE)
8
-
9
- **QAST** is a small, ORM-agnostic library that parses human-readable query strings (e.g. `age gt 25 and (name eq "John" or city eq "Paris")`) into an **Abstract Syntax Tree (AST)** and then transforms that AST into **ORM-compatible filter objects** such as Prisma or TypeORM filters.
10
-
11
- It aims to provide a secure, declarative, and type-safe way to support advanced filtering in REST APIs — without falling into the pitfalls of raw string-based query patterns.
12
-
13
- ## Features
14
-
15
- - 🔒 **Safe**: Validates operators, values, and fields against whitelists
16
- - 🎯 **Type-Safe**: Full TypeScript support for parsed ASTs and generated filters
17
- - 🔌 **ORM-Agnostic**: Works with Prisma, TypeORM, Sequelize, and more via adapters
18
- - 📝 **Simple Syntax**: Natural query expressions using logical operators
19
- - 🚀 **Lightweight**: No dependencies, small bundle size
20
-
21
- ## Installation
22
-
23
- ```bash
24
- npm install qast
25
- ```
26
-
27
- ## Quick Start
28
-
29
- ### Basic Usage
30
-
31
- ```typescript
32
- import { parseQuery, toPrismaFilter } from 'qast';
33
-
34
- const query = 'age gt 25 and (name eq "John" or city eq "Paris")';
35
-
36
- const ast = parseQuery(query);
37
- const prismaFilter = toPrismaFilter(ast);
38
-
39
- await prisma.user.findMany(prismaFilter);
40
- ```
41
-
42
- ### With Validation
43
-
44
- ```typescript
45
- import { parseQuery, toPrismaFilter } from 'qast';
46
-
47
- const query = 'age gt 25 and name eq "John"';
48
-
49
- // Parse with whitelist validation
50
- const ast = parseQuery(query, {
51
- allowedFields: ['age', 'name', 'city'],
52
- allowedOperators: ['gt', 'eq', 'lt'],
53
- validate: true,
54
- });
55
-
56
- const prismaFilter = toPrismaFilter(ast);
57
- await prisma.user.findMany(prismaFilter);
58
- ```
59
-
60
- ## Query Syntax
61
-
62
- ### Operators
63
-
64
- QAST supports the following comparison operators:
65
-
66
- - `eq` - Equal
67
- - `ne` - Not equal
68
- - `gt` - Greater than
69
- - `lt` - Less than
70
- - `gte` - Greater than or equal
71
- - `lte` - Less than or equal
72
- - `in` - In array
73
- - `contains` - Contains substring (string matching)
74
-
75
- ### Logical Operators
76
-
77
- - `and` - Logical AND
78
- - `or` - Logical OR
79
-
80
- ### Values
81
-
82
- - **Strings**: Use single or double quotes: `"John"` or `'John'`
83
- - **Numbers**: Integers or floats: `25`, `25.99`, `-10`
84
- - **Booleans**: `true` or `false`
85
- - **Arrays**: For `in` operator: `[1,2,3]` or `["John","Jane"]`
86
-
87
- ### Examples
88
-
89
- ```typescript
90
- // Simple comparison
91
- 'age gt 25'
92
-
93
- // String comparison
94
- 'name eq "John"'
95
-
96
- // Boolean comparison
97
- 'active eq true'
98
-
99
- // Array (in operator)
100
- 'age in [1,2,3]'
101
-
102
- // AND operation
103
- 'age gt 25 and name eq "John"'
104
-
105
- // OR operation
106
- 'name eq "John" or name eq "Jane"'
107
-
108
- // Nested parentheses
109
- 'age gt 25 and (name eq "John" or city eq "Paris")'
110
-
111
- // Complex query
112
- 'age gt 25 and (name eq "John" or city eq "Paris") and active eq true'
113
- ```
114
-
115
- ## ORM Adapters
116
-
117
- ### Prisma
118
-
119
- ```typescript
120
- import { parseQuery, toPrismaFilter } from 'qast';
121
-
122
- const query = 'age gt 25 and name eq "John"';
123
- const ast = parseQuery(query);
124
- const filter = toPrismaFilter(ast);
125
-
126
- // filter = {
127
- // where: {
128
- // age: { gt: 25 },
129
- // name: { equals: "John" }
130
- // }
131
- // }
132
-
133
- await prisma.user.findMany(filter);
134
- ```
135
-
136
- ### TypeORM
137
-
138
- ```typescript
139
- import { parseQuery, toTypeORMFilter } from 'qast';
140
- import { MoreThan, Equal } from 'typeorm';
141
-
142
- const query = 'age gt 25 and name eq "John"';
143
- const ast = parseQuery(query);
144
- const filter = toTypeORMFilter(ast);
145
-
146
- // Note: TypeORM requires operator functions for non-equality comparisons
147
- // The adapter returns a structure that you can transform using TypeORM operators
148
- // For equality, TypeORM accepts plain values directly
149
-
150
- // filter.where = {
151
- // age: { __qast_operator__: 'gt', value: 25 },
152
- // name: "John"
153
- // }
154
-
155
- // Transform to use TypeORM operators:
156
- // const transformed = {
157
- // age: MoreThan(25),
158
- // name: "John"
159
- // }
160
-
161
- await userRepository.find({ where: transformed });
162
- ```
163
-
164
- **Note**: TypeORM requires operator functions (`MoreThan`, `LessThan`, etc.) for non-equality comparisons. The adapter returns a structure with metadata that you can transform. For equality comparisons, TypeORM accepts plain values.
165
-
166
- ### Sequelize
167
-
168
- ```typescript
169
- import { parseQuery, toSequelizeFilter } from 'qast';
170
- import { Op } from 'sequelize';
171
-
172
- const query = 'age gt 25 and name eq "John"';
173
- const ast = parseQuery(query);
174
- const filter = toSequelizeFilter(ast);
175
-
176
- // filter = {
177
- // __qast_logical__: 'and',
178
- // conditions: [
179
- // { age: { __qast_operator__: 'gt', value: 25 } },
180
- // { name: 'John' }
181
- // ]
182
- // }
183
-
184
- // Transform to use Sequelize Op operators:
185
- function transformSequelizeFilter(filter: any): any {
186
- if (filter.__qast_logical__) {
187
- const op = filter.__qast_logical__ === 'and' ? Op.and : Op.or;
188
- return {
189
- [op]: filter.conditions.map(transformSequelizeFilter),
190
- };
191
- }
192
-
193
- const result: any = {};
194
- for (const [key, value] of Object.entries(filter)) {
195
- if (value && typeof value === 'object' && '__qast_operator__' in value) {
196
- const opKey = value.__qast_operator__;
197
- const op = Op[opKey as keyof typeof Op];
198
- if (opKey === 'contains') {
199
- result[key] = { [Op.like]: `%${value.value}%` };
200
- } else {
201
- result[key] = { [op]: value.value };
202
- }
203
- } else {
204
- result[key] = value;
205
- }
206
- }
207
- return result;
208
- }
209
-
210
- const transformed = transformSequelizeFilter(filter);
211
- // transformed = {
212
- // [Op.and]: [
213
- // { age: { [Op.gt]: 25 } },
214
- // { name: 'John' }
215
- // ]
216
- // }
217
-
218
- await User.findAll({ where: transformed });
219
- ```
220
-
221
- **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
-
223
- ## API Reference
224
-
225
- ### `parseQuery(query: string, options?: ParseOptions): QastNode`
226
-
227
- Parse a query string into an AST.
228
-
229
- **Parameters:**
230
- - `query` - The query string to parse
231
- - `options` - Optional parsing options:
232
- - `allowedFields?: string[]` - Whitelist of allowed field names
233
- - `allowedOperators?: Operator[]` - Whitelist of allowed operators
234
- - `validate?: boolean` - Whether to validate against whitelists (default: true if whitelists are provided)
235
- - `maxDepth?: number` - Maximum allowed AST depth (to limit nested logical expressions)
236
- - `maxNodes?: number` - Maximum allowed number of AST nodes (to limit overall query complexity)
237
- - `maxQueryLength?: number` - Maximum allowed length of the raw query string (checked before parsing)
238
- - `maxArrayLength?: number` - Maximum allowed length of array values (for `in` operator)
239
- - `maxStringLength?: number` - Maximum allowed length of string values
240
-
241
- **Returns:** The parsed AST node
242
-
243
- **Example:**
244
- ```typescript
245
- const ast = parseQuery('age gt 25', {
246
- allowedFields: ['age', 'name'],
247
- allowedOperators: ['gt', 'eq'],
248
- validate: true,
249
- maxDepth: 5,
250
- maxNodes: 50,
251
- maxQueryLength: 1000,
252
- maxArrayLength: 100,
253
- maxStringLength: 200,
254
- });
255
- ```
256
-
257
- ### `toPrismaFilter(ast: QastNode): PrismaFilter`
258
-
259
- Transform an AST to a Prisma filter.
260
-
261
- **Returns:** Prisma filter object with `where` property
262
-
263
- ### `toTypeORMFilter(ast: QastNode): TypeORMFilter`
264
-
265
- Transform an AST to a TypeORM filter.
266
-
267
- **Returns:** TypeORM filter object with `where` property
268
-
269
- **Note:** TypeORM requires operator functions for non-equality comparisons. You may need to transform the result.
270
-
271
- ### `toSequelizeFilter(ast: QastNode): SequelizeFilter`
272
-
273
- Transform an AST to a Sequelize filter.
274
-
275
- **Returns:** Sequelize filter object
276
-
277
- **Note:** Sequelize uses the `Op` object. You need to transform `$`-prefixed operators to use `Op` operators.
278
-
279
- ### `validateQuery(ast: QastNode, whitelist: WhitelistOptions): void`
280
-
281
- Validate an AST against whitelists.
282
-
283
- **Parameters:**
284
- - `ast` - The AST to validate
285
- - `whitelist` - Whitelist options:
286
- - `allowedFields?: string[]` - Allowed field names
287
- - `allowedOperators?: Operator[]` - Allowed operators
288
-
289
- **Throws:** `ValidationError` if validation fails
290
-
291
- ### `extractFields(ast: QastNode): string[]`
292
-
293
- Extract all field names used in an AST.
294
-
295
- **Returns:** Array of unique field names
296
-
297
- ### `extractOperators(ast: QastNode): Operator[]`
298
-
299
- Extract all operators used in an AST.
300
-
301
- **Returns:** Array of unique operators
302
-
303
- ### `validateQueryComplexity(ast: QastNode, options: ComplexityOptions): void`
304
-
305
- Validate an AST against complexity limits.
306
-
307
- **Parameters:**
308
- - `ast` - The AST to validate
309
- - `options` - Complexity options:
310
- - `maxDepth?: number` - Maximum allowed AST depth
311
- - `maxNodes?: number` - Maximum allowed number of nodes
312
- - `maxArrayLength?: number` - Maximum allowed array length
313
- - `maxStringLength?: number` - Maximum allowed string length
314
-
315
- **Throws:** `ValidationError` if any limit is exceeded
316
-
317
- **Example:**
318
- ```typescript
319
- import { parseQuery, validateQueryComplexity } from 'qast';
320
-
321
- const ast = parseQuery('age in [1,2,3,4,5]');
322
- validateQueryComplexity(ast, {
323
- maxDepth: 5,
324
- maxNodes: 20,
325
- maxArrayLength: 100,
326
- maxStringLength: 200,
327
- });
328
- ```
329
-
330
- ## Security Best Practices
331
-
332
- 1. **Always use whitelists**: Restrict which fields and operators can be used in queries.
333
-
334
- ```typescript
335
- const ast = parseQuery(req.query.filter, {
336
- allowedFields: ['age', 'name', 'city'],
337
- allowedOperators: ['gt', 'eq', 'lt'],
338
- validate: true,
339
- });
340
- ```
341
-
342
- 2. **Validate user input**: Don't trust user-provided query strings without validation.
343
-
344
- 3. **Limit query complexity**: Use complexity limits to prevent DoS attacks.
345
-
346
- ```typescript
347
- const ast = parseQuery(req.query.filter, {
348
- allowedFields: ['age', 'name', 'city'],
349
- allowedOperators: ['gt', 'eq', 'lt', 'in'],
350
- validate: true,
351
- // Complexity limits
352
- maxQueryLength: 1000, // Reject queries longer than 1000 chars
353
- maxDepth: 5, // Max 5 levels of nesting
354
- maxNodes: 20, // Max 20 conditions
355
- maxArrayLength: 100, // Max 100 items in 'in' arrays
356
- maxStringLength: 200, // Max 200 chars per string value
357
- });
358
- ```
359
-
360
- 4. **Use type checking**: Ensure values match expected types for fields.
361
-
362
- ## Error Handling
363
-
364
- QAST provides custom error classes:
365
-
366
- - `ParseError` - Syntax errors in query strings
367
- - `ValidationError` - Validation failures (disallowed fields/operators)
368
- - `TokenizationError` - Tokenization errors
369
-
370
- ```typescript
371
- import { parseQuery, ParseError, ValidationError } from 'qast';
372
-
373
- try {
374
- const ast = parseQuery(query, { allowedFields: ['age'], validate: true });
375
- } catch (error) {
376
- if (error instanceof ParseError) {
377
- console.error('Parse error:', error.message);
378
- } else if (error instanceof ValidationError) {
379
- console.error('Validation error:', error.message);
380
- console.error('Field:', error.field);
381
- console.error('Operator:', error.operator);
382
- }
383
- }
384
- ```
385
-
386
- ## TypeScript Support
387
-
388
- QAST is written in TypeScript and provides full type definitions:
389
-
390
- ```typescript
391
- import { QastNode, ComparisonNode, LogicalNode, Operator } from 'qast';
392
-
393
- function processNode(node: QastNode): void {
394
- if (node.type === 'COMPARISON') {
395
- const comparison = node as ComparisonNode;
396
- console.log(comparison.field, comparison.op, comparison.value);
397
- } else if (node.type === 'AND' || node.type === 'OR') {
398
- const logical = node as LogicalNode;
399
- processNode(logical.left);
400
- processNode(logical.right);
401
- }
402
- }
403
- ```
404
-
405
- ## Examples
406
-
407
- ### REST API Endpoint
408
-
409
- ```typescript
410
- import { parseQuery, toPrismaFilter } from 'qast';
411
- import { PrismaClient } from '@prisma/client';
412
-
413
- const prisma = new PrismaClient();
414
-
415
- app.get('/users', async (req, res) => {
416
- try {
417
- const query = req.query.filter as string;
418
-
419
- // Parse and validate query
420
- const ast = parseQuery(query, {
421
- allowedFields: ['age', 'name', 'city', 'active'],
422
- allowedOperators: ['gt', 'lt', 'eq', 'in'],
423
- validate: true,
424
- });
425
-
426
- // Transform to Prisma filter
427
- const filter = toPrismaFilter(ast);
428
-
429
- // Query database
430
- const users = await prisma.user.findMany(filter);
431
-
432
- res.json(users);
433
- } catch (error) {
434
- if (error instanceof ValidationError) {
435
- res.status(400).json({ error: error.message });
436
- } else {
437
- res.status(500).json({ error: 'Internal server error' });
438
- }
439
- }
440
- });
441
- ```
442
-
443
- ### Express Middleware
444
-
445
- ```typescript
446
- import { parseQuery, toPrismaFilter, ValidationError } from 'qast';
447
-
448
- function qastMiddleware(allowedFields: string[], allowedOperators: Operator[]) {
449
- return (req, res, next) => {
450
- try {
451
- if (req.query.filter) {
452
- const ast = parseQuery(req.query.filter, {
453
- allowedFields,
454
- allowedOperators,
455
- validate: true,
456
- });
457
-
458
- req.qastFilter = toPrismaFilter(ast);
459
- }
460
- next();
461
- } catch (error) {
462
- if (error instanceof ValidationError) {
463
- res.status(400).json({ error: error.message });
464
- } else {
465
- next(error);
466
- }
467
- }
468
- };
469
- }
470
- ```
471
-
472
- ## Comparison with Alternatives
473
-
474
- ### Why QAST?
475
-
476
- | Feature | QAST | GraphQL | OData | Custom Parsers |
477
- |---------|------|---------|-------|----------------|
478
- | **Type Safety** | ✅ Full TypeScript | ❌ Runtime only | ⚠️ Partial | ❌ Usually none |
479
- | **Security** | ✅ Whitelist validation | ✅ Built-in | ✅ Built-in | ⚠️ Manual |
480
- | **ORM Agnostic** | Yes | ❌ No | ❌ No | ⚠️ Varies |
481
- | **Zero Dependencies** | ✅ Yes | ❌ No | ❌ No | ⚠️ Varies |
482
- | **Learning Curve** | Simple | ❌ Complex | ❌ Complex | ⚠️ Varies |
483
- | **REST API Friendly** | ✅ Yes | ❌ Requires GraphQL endpoint | ✅ Yes | ⚠️ Varies |
484
- | **Bundle Size** | < 10KB | Large | Large | ⚠️ Varies |
485
-
486
- **Use QAST when:**
487
- - You want a simple, secure query language for REST APIs
488
- - You need type-safe query parsing with TypeScript
489
- - You're using Prisma, TypeORM, or Sequelize
490
- - You want zero dependencies and a small bundle size
491
- - You need field and operator whitelisting for security
492
-
493
- **Consider alternatives when:**
494
- - You need GraphQL's full query capabilities
495
- - You require standardized query protocols (OData)
496
- - You have complex nested data relationships
497
-
498
- ## Examples
499
-
500
- See the [`examples`](./examples) directory for complete, working examples:
501
-
502
- - [Express.js REST API](./examples/express) - Full Express.js server with Prisma
503
- - [Next.js API Routes](./examples/nextjs) - Next.js API routes with QAST
504
- - [NestJS Integration](./examples/nestjs) - NestJS controller with query filtering
505
- - [Interactive Playground](./examples/playground.html) - Try QAST queries in your browser (demo)
506
-
507
- ## License
508
-
509
- MIT © 2025
510
-
511
- ## Contributing
512
-
513
- Contributions are welcome! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for details.
514
-
515
- - GitHub Repository: https://github.com/hocestnonsatis/qast
516
- - Issues: https://github.com/hocestnonsatis/qast/issues
517
-
518
- ## Acknowledgments
519
-
520
- QAST is inspired by the need for safe, type-safe query parsing in REST APIs. It aims to provide a lightweight alternative to complex query protocols while maintaining security and developer experience.
1
+ # QAST — Query to AST to ORM
2
+
3
+ [![npm version](https://img.shields.io/npm/v/qast.svg)](https://www.npmjs.com/package/qast)
4
+ [![npm downloads](https://img.shields.io/npm/dm/qast.svg)](https://www.npmjs.com/package/qast)
5
+ [![GitHub stars](https://img.shields.io/github/stars/hocestnonsatis/qast.svg)](https://github.com/hocestnonsatis/qast)
6
+ [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/)
7
+ [![License: MIT](https://img.shields.io/npm/l/qast.svg)](https://github.com/hocestnonsatis/qast/blob/main/LICENSE)
8
+
9
+ **QAST** is a small, ORM-agnostic library that parses human-readable query strings (e.g. `age gt 25 and (name eq "John" or city eq "Paris")`) into an **Abstract Syntax Tree (AST)** and then transforms that AST into **ORM-compatible filter objects** such as Prisma or TypeORM filters.
10
+
11
+ It aims to provide a secure, declarative, and type-safe way to support advanced filtering in REST APIs — without falling into the pitfalls of raw string-based query patterns.
12
+
13
+ ## Features
14
+
15
+ - 🔒 **Safe**: Validates operators, values, and fields against whitelists
16
+ - 🎯 **Type-Safe**: Full TypeScript support for parsed ASTs and generated filters
17
+ - 🔌 **ORM-Agnostic**: Works with Prisma, TypeORM, Sequelize, and more via adapters
18
+ - 📝 **Simple Syntax**: Natural query expressions using logical operators
19
+ - 🚀 **Lightweight**: No dependencies, small bundle size
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install qast
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ ### Basic Usage
30
+
31
+ ```typescript
32
+ import { parseQuery, toPrismaFilter } from 'qast';
33
+
34
+ const query = 'age gt 25 and (name eq "John" or city eq "Paris")';
35
+
36
+ const ast = parseQuery(query);
37
+ const prismaFilter = toPrismaFilter(ast);
38
+
39
+ await prisma.user.findMany(prismaFilter);
40
+ ```
41
+
42
+ ### With Validation
43
+
44
+ ```typescript
45
+ import { parseQuery, toPrismaFilter } from 'qast';
46
+
47
+ const query = 'age gt 25 and name eq "John"';
48
+
49
+ // Parse with whitelist validation
50
+ const ast = parseQuery(query, {
51
+ allowedFields: ['age', 'name', 'city'],
52
+ allowedOperators: ['gt', 'eq', 'lt'],
53
+ validate: true,
54
+ });
55
+
56
+ const prismaFilter = toPrismaFilter(ast);
57
+ await prisma.user.findMany(prismaFilter);
58
+ ```
59
+
60
+ ## Query Syntax
61
+
62
+ ### Operators
63
+
64
+ QAST supports the following comparison operators:
65
+
66
+ - `eq` - Equal
67
+ - `ne` - Not equal
68
+ - `gt` - Greater than
69
+ - `lt` - Less than
70
+ - `gte` - Greater than or equal
71
+ - `lte` - Less than or equal
72
+ - `in` - In array
73
+ - `notIn` - Not in array
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`
82
+
83
+ ### Logical Operators
84
+
85
+ - `and` - Logical AND
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
94
+
95
+ ### Values
96
+
97
+ - **Strings**: Use single or double quotes: `"John"` or `'John'`
98
+ - **Numbers**: Integers or floats: `25`, `25.99`, `-10`
99
+ - **Booleans**: `true` or `false`
100
+ - **Null**: `null` - For null checks
101
+ - **Arrays**: For `in` and `notIn` operators: `[1,2,3]` or `["John","Jane"]`
102
+
103
+ ### Examples
104
+
105
+ ```typescript
106
+ // Simple comparison
107
+ 'age gt 25'
108
+
109
+ // String comparison
110
+ 'name eq "John"'
111
+
112
+ // Boolean comparison
113
+ 'active eq true'
114
+
115
+ // Array (in operator)
116
+ 'age in [1,2,3]'
117
+
118
+ // AND operation
119
+ 'age gt 25 and name eq "John"'
120
+
121
+ // OR operation
122
+ 'name eq "John" or name eq "Jane"'
123
+
124
+ // Nested parentheses
125
+ 'age gt 25 and (name eq "John" or city eq "Paris")'
126
+
127
+ // Complex query
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'
142
+ ```
143
+
144
+ ## ORM Adapters
145
+
146
+ ### Prisma
147
+
148
+ ```typescript
149
+ import { parseQuery, toPrismaFilter } from 'qast';
150
+
151
+ const query = 'age gt 25 and name eq "John"';
152
+ const ast = parseQuery(query);
153
+ const filter = toPrismaFilter(ast);
154
+
155
+ // filter = {
156
+ // where: {
157
+ // age: { gt: 25 },
158
+ // name: { equals: "John" }
159
+ // }
160
+ // }
161
+
162
+ await prisma.user.findMany(filter);
163
+ ```
164
+
165
+ ### TypeORM
166
+
167
+ ```typescript
168
+ import { parseQuery, toTypeORMFilter } from 'qast';
169
+ import { MoreThan, Equal } from 'typeorm';
170
+
171
+ const query = 'age gt 25 and name eq "John"';
172
+ const ast = parseQuery(query);
173
+ const filter = toTypeORMFilter(ast);
174
+
175
+ // Note: TypeORM requires operator functions for non-equality comparisons
176
+ // The adapter returns a structure that you can transform using TypeORM operators
177
+ // For equality, TypeORM accepts plain values directly
178
+
179
+ // filter.where = {
180
+ // age: { __qast_operator__: 'gt', value: 25 },
181
+ // name: "John"
182
+ // }
183
+
184
+ // Transform to use TypeORM operators:
185
+ // const transformed = {
186
+ // age: MoreThan(25),
187
+ // name: "John"
188
+ // }
189
+
190
+ await userRepository.find({ where: transformed });
191
+ ```
192
+
193
+ **Note**: TypeORM requires operator functions (`MoreThan`, `LessThan`, etc.) for non-equality comparisons. The adapter returns a structure with metadata that you can transform. For equality comparisons, TypeORM accepts plain values.
194
+
195
+ ### Sequelize
196
+
197
+ ```typescript
198
+ import { parseQuery, toSequelizeFilter } from 'qast';
199
+ import { Op } from 'sequelize';
200
+
201
+ const query = 'age gt 25 and name eq "John"';
202
+ const ast = parseQuery(query);
203
+ const filter = toSequelizeFilter(ast);
204
+
205
+ // filter = {
206
+ // __qast_logical__: 'and',
207
+ // conditions: [
208
+ // { age: { __qast_operator__: 'gt', value: 25 } },
209
+ // { name: 'John' }
210
+ // ]
211
+ // }
212
+
213
+ // Transform to use Sequelize Op operators:
214
+ function transformSequelizeFilter(filter: any): any {
215
+ if (filter.__qast_logical__) {
216
+ const op = filter.__qast_logical__ === 'and' ? Op.and : Op.or;
217
+ return {
218
+ [op]: filter.conditions.map(transformSequelizeFilter),
219
+ };
220
+ }
221
+
222
+ const result: any = {};
223
+ for (const [key, value] of Object.entries(filter)) {
224
+ if (value && typeof value === 'object' && '__qast_operator__' in value) {
225
+ const opKey = value.__qast_operator__;
226
+ const op = Op[opKey as keyof typeof Op];
227
+ if (opKey === 'contains') {
228
+ result[key] = { [Op.like]: `%${value.value}%` };
229
+ } else {
230
+ result[key] = { [op]: value.value };
231
+ }
232
+ } else {
233
+ result[key] = value;
234
+ }
235
+ }
236
+ return result;
237
+ }
238
+
239
+ const transformed = transformSequelizeFilter(filter);
240
+ // transformed = {
241
+ // [Op.and]: [
242
+ // { age: { [Op.gt]: 25 } },
243
+ // { name: 'John' }
244
+ // ]
245
+ // }
246
+
247
+ await User.findAll({ where: transformed });
248
+ ```
249
+
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.
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
+
438
+ ## API Reference
439
+
440
+ ### `parseQuery(query: string, options?: ParseOptions): QastNode`
441
+
442
+ Parse a query string into an AST.
443
+
444
+ **Parameters:**
445
+ - `query` - The query string to parse
446
+ - `options` - Optional parsing options:
447
+ - `allowedFields?: string[]` - Whitelist of allowed field names
448
+ - `allowedOperators?: Operator[]` - Whitelist of allowed operators
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
455
+
456
+ **Returns:** The parsed AST node
457
+
458
+ **Example:**
459
+ ```typescript
460
+ const ast = parseQuery('age gt 25', {
461
+ allowedFields: ['age', 'name'],
462
+ allowedOperators: ['gt', 'eq'],
463
+ validate: true,
464
+ maxDepth: 5,
465
+ maxNodes: 50,
466
+ maxQueryLength: 1000,
467
+ maxArrayLength: 100,
468
+ maxStringLength: 200,
469
+ });
470
+ ```
471
+
472
+ ### `toPrismaFilter(ast: QastNode): PrismaFilter`
473
+
474
+ Transform an AST to a Prisma filter.
475
+
476
+ **Returns:** Prisma filter object with `where` property
477
+
478
+ ### `toTypeORMFilter(ast: QastNode): TypeORMFilter`
479
+
480
+ Transform an AST to a TypeORM filter.
481
+
482
+ **Returns:** TypeORM filter object with `where` property
483
+
484
+ **Note:** TypeORM requires operator functions for non-equality comparisons. You may need to transform the result.
485
+
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`
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.
533
+
534
+ ### `validateQuery(ast: QastNode, whitelist: WhitelistOptions): void`
535
+
536
+ Validate an AST against whitelists.
537
+
538
+ **Parameters:**
539
+ - `ast` - The AST to validate
540
+ - `whitelist` - Whitelist options:
541
+ - `allowedFields?: string[]` - Allowed field names
542
+ - `allowedOperators?: Operator[]` - Allowed operators
543
+
544
+ **Throws:** `ValidationError` if validation fails
545
+
546
+ ### `extractFields(ast: QastNode): string[]`
547
+
548
+ Extract all field names used in an AST.
549
+
550
+ **Returns:** Array of unique field names
551
+
552
+ ### `extractOperators(ast: QastNode): Operator[]`
553
+
554
+ Extract all operators used in an AST.
555
+
556
+ **Returns:** Array of unique operators
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
+
585
+ ## Security Best Practices
586
+
587
+ 1. **Always use whitelists**: Restrict which fields and operators can be used in queries.
588
+
589
+ ```typescript
590
+ const ast = parseQuery(req.query.filter, {
591
+ allowedFields: ['age', 'name', 'city'],
592
+ allowedOperators: ['gt', 'eq', 'lt'],
593
+ validate: true,
594
+ });
595
+ ```
596
+
597
+ 2. **Validate user input**: Don't trust user-provided query strings without validation.
598
+
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
+ ```
614
+
615
+ 4. **Use type checking**: Ensure values match expected types for fields.
616
+
617
+ ## Error Handling
618
+
619
+ QAST provides custom error classes:
620
+
621
+ - `ParseError` - Syntax errors in query strings
622
+ - `ValidationError` - Validation failures (disallowed fields/operators)
623
+ - `TokenizationError` - Tokenization errors
624
+
625
+ ```typescript
626
+ import { parseQuery, ParseError, ValidationError } from 'qast';
627
+
628
+ try {
629
+ const ast = parseQuery(query, { allowedFields: ['age'], validate: true });
630
+ } catch (error) {
631
+ if (error instanceof ParseError) {
632
+ console.error('Parse error:', error.message);
633
+ } else if (error instanceof ValidationError) {
634
+ console.error('Validation error:', error.message);
635
+ console.error('Field:', error.field);
636
+ console.error('Operator:', error.operator);
637
+ }
638
+ }
639
+ ```
640
+
641
+ ## TypeScript Support
642
+
643
+ QAST is written in TypeScript and provides full type definitions:
644
+
645
+ ```typescript
646
+ import { QastNode, ComparisonNode, LogicalNode, Operator } from 'qast';
647
+
648
+ function processNode(node: QastNode): void {
649
+ if (node.type === 'COMPARISON') {
650
+ const comparison = node as ComparisonNode;
651
+ console.log(comparison.field, comparison.op, comparison.value);
652
+ } else if (node.type === 'AND' || node.type === 'OR') {
653
+ const logical = node as LogicalNode;
654
+ processNode(logical.left);
655
+ processNode(logical.right);
656
+ }
657
+ }
658
+ ```
659
+
660
+ ## Examples
661
+
662
+ ### REST API endpoint
663
+
664
+ ```typescript
665
+ import { parseQuery, toPrismaFilter } from 'qast';
666
+ import { PrismaClient } from '@prisma/client';
667
+
668
+ const prisma = new PrismaClient();
669
+
670
+ app.get('/users', async (req, res) => {
671
+ try {
672
+ const query = req.query.filter as string;
673
+
674
+ // Parse and validate query
675
+ const ast = parseQuery(query, {
676
+ allowedFields: ['age', 'name', 'city', 'active'],
677
+ allowedOperators: ['gt', 'lt', 'eq', 'in'],
678
+ validate: true,
679
+ });
680
+
681
+ // Transform to Prisma filter
682
+ const filter = toPrismaFilter(ast);
683
+
684
+ // Query database
685
+ const users = await prisma.user.findMany(filter);
686
+
687
+ res.json(users);
688
+ } catch (error) {
689
+ if (error instanceof ValidationError) {
690
+ res.status(400).json({ error: error.message });
691
+ } else {
692
+ res.status(500).json({ error: 'Internal server error' });
693
+ }
694
+ }
695
+ });
696
+ ```
697
+
698
+ ### Express middleware
699
+
700
+ ```typescript
701
+ import { parseQuery, toPrismaFilter, ValidationError } from 'qast';
702
+
703
+ function qastMiddleware(allowedFields: string[], allowedOperators: Operator[]) {
704
+ return (req, res, next) => {
705
+ try {
706
+ if (req.query.filter) {
707
+ const ast = parseQuery(req.query.filter, {
708
+ allowedFields,
709
+ allowedOperators,
710
+ validate: true,
711
+ });
712
+
713
+ req.qastFilter = toPrismaFilter(ast);
714
+ }
715
+ next();
716
+ } catch (error) {
717
+ if (error instanceof ValidationError) {
718
+ res.status(400).json({ error: error.message });
719
+ } else {
720
+ next(error);
721
+ }
722
+ }
723
+ };
724
+ }
725
+ ```
726
+
727
+ ## Comparison with Alternatives
728
+
729
+ ### Why QAST?
730
+
731
+ | Feature | QAST | GraphQL | OData | Custom Parsers |
732
+ |---------|------|---------|-------|----------------|
733
+ | **Type Safety** | ✅ Full TypeScript | ❌ Runtime only | ⚠️ Partial | ❌ Usually none |
734
+ | **Security** | ✅ Whitelist validation | ✅ Built-in | ✅ Built-in | ⚠️ Manual |
735
+ | **ORM Agnostic** | ✅ Yes | ❌ No | ❌ No | ⚠️ Varies |
736
+ | **Zero Dependencies** | ✅ Yes | ❌ No | ❌ No | ⚠️ Varies |
737
+ | **Learning Curve** | ✅ Simple | ❌ Complex | ❌ Complex | ⚠️ Varies |
738
+ | **REST API Friendly** | ✅ Yes | ❌ Requires GraphQL endpoint | ✅ Yes | ⚠️ Varies |
739
+ | **Bundle Size** | ✅ < 10KB | ❌ Large | ❌ Large | ⚠️ Varies |
740
+
741
+ **Use QAST when:**
742
+ - You want a simple, secure query language for REST APIs
743
+ - You need type-safe query parsing with TypeScript
744
+ - You're using Prisma, TypeORM, or Sequelize
745
+ - You want zero dependencies and a small bundle size
746
+ - You need field and operator whitelisting for security
747
+
748
+ **Consider alternatives when:**
749
+ - You need GraphQL's full query capabilities
750
+ - You require standardized query protocols (OData)
751
+ - You have complex nested data relationships
752
+
753
+ ### Example Projects
754
+
755
+ See the [`examples`](./examples) directory for complete, working examples:
756
+
757
+ - [Express.js REST API](./examples/express) — Full Express.js server with Prisma
758
+ - [Next.js API Routes](./examples/nextjs) — Next.js API routes with QAST
759
+ - [NestJS Integration](./examples/nestjs) — NestJS controller with query filtering
760
+ - [Interactive Playground](./examples/playground.html) — Try QAST queries in your browser
761
+
762
+ ## License
763
+
764
+ MIT © 2025
765
+
766
+ ## Contributing
767
+
768
+ Contributions are welcome! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for details.
769
+
770
+ - GitHub Repository: https://github.com/hocestnonsatis/qast
771
+ - Issues: https://github.com/hocestnonsatis/qast/issues
772
+
773
+ ## Acknowledgments
774
+
775
+ QAST is inspired by the need for safe, type-safe query parsing in REST APIs. It aims to provide a lightweight alternative to complex query protocols while maintaining security and developer experience.