qast 2.0.2 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # QAST — Query to AST to ORM
1
+ ## QAST — Query to AST to ORM
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/qast.svg)](https://www.npmjs.com/package/qast)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/qast.svg)](https://www.npmjs.com/package/qast)
@@ -6,770 +6,84 @@
6
6
  [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/)
7
7
  [![License: MIT](https://img.shields.io/npm/l/qast.svg)](https://github.com/hocestnonsatis/qast/blob/main/LICENSE)
8
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.
9
+ **QAST** is a small, ORM-agnostic, zero-dependency library that turns humanreadable query strings
10
+ (for example: `age gt 25 and (name eq "John" or city eq "Paris")`) into an **AST** and then into **ORM filters**.
10
11
 
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
+ It is designed to be simple to adopt, safe by default, and easy to integrate into existing REST APIs.
12
13
 
13
- ## Features
14
+ ---
14
15
 
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
16
+ ### Features
20
17
 
21
- ## Installation
18
+ - **Zero runtime dependencies** – lightweight and easy to embed
19
+ - **ORM‑agnostic adapters** – Prisma, TypeORM, Sequelize, Mongoose, Knex, Drizzle
20
+ - **Safe by default** – optional whitelists and complexity limits
21
+ - **TypeScript first** – fully typed API and AST
22
+
23
+ ---
24
+
25
+ ### Installation
22
26
 
23
27
  ```bash
24
28
  npm install qast
25
29
  ```
26
30
 
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);
31
+ ---
38
32
 
39
- await prisma.user.findMany(prismaFilter);
40
- ```
41
-
42
- ### With Validation
33
+ ### Quick Start (Prisma example)
43
34
 
44
35
  ```typescript
45
36
  import { parseQuery, toPrismaFilter } from 'qast';
46
37
 
47
- const query = 'age gt 25 and name eq "John"';
38
+ // Example: ?filter=age gt 25 and name eq "John"
39
+ const raw = req.query.filter as string;
48
40
 
49
- // Parse with whitelist validation
50
- const ast = parseQuery(query, {
51
- allowedFields: ['age', 'name', 'city'],
52
- allowedOperators: ['gt', 'eq', 'lt'],
41
+ // Parse and (optionally) validate
42
+ const ast = parseQuery(raw, {
43
+ allowedFields: ['age', 'name', 'city', 'active'],
44
+ allowedOperators: ['eq', 'ne', 'gt', 'lt', 'gte', 'lte', 'in'],
53
45
  validate: true,
54
46
  });
55
47
 
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);
48
+ // Transform to Prisma filter
153
49
  const filter = toPrismaFilter(ast);
154
50
 
155
- // filter = {
156
- // where: {
157
- // age: { gt: 25 },
158
- // name: { equals: "John" }
159
- // }
160
- // }
161
-
162
- await prisma.user.findMany(filter);
51
+ const users = await prisma.user.findMany(filter);
163
52
  ```
164
53
 
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);
54
+ ---
174
55
 
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
56
+ ### Core Concepts
178
57
 
179
- // filter.where = {
180
- // age: { __qast_operator__: 'gt', value: 25 },
181
- // name: "John"
182
- // }
58
+ - **Query string** → parsed into an **AST**
59
+ - AST can be:
60
+ - used directly, or
61
+ - passed to an adapter (Prisma, TypeORM, etc.) to get an ORM‑specific filter
62
+ - Security and performance can be tuned via:
63
+ - `allowedFields`, `allowedOperators`
64
+ - `maxDepth`, `maxNodes`, `maxQueryLength`, `maxArrayLength`, `maxStringLength`
183
65
 
184
- // Transform to use TypeORM operators:
185
- // const transformed = {
186
- // age: MoreThan(25),
187
- // name: "John"
188
- // }
66
+ You do **not** need any of the ORMs installed if you only work with the AST; ORM packages are optional peer dependencies.
189
67
 
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.
68
+ ---
194
69
 
195
- ### Sequelize
196
-
197
- ```typescript
198
- import { parseQuery, toSequelizeFilter } from 'qast';
199
- import { Op } from 'sequelize';
70
+ ### Minimal API Surface
200
71
 
201
- const query = 'age gt 25 and name eq "John"';
202
- const ast = parseQuery(query);
203
- const filter = toSequelizeFilter(ast);
72
+ - `parseQuery(query, options?)` parse a filter expression into an AST
73
+ - `parseQueryFull(query, options?)` – parse filters + `orderBy` / `limit` / `offset`
74
+ - `toPrismaFilter(ast)` – convert AST / QueryAST to Prisma filter object
75
+ - `toTypeORMFilter(ast)` – convert AST / QueryAST to a TypeORM‑style filter
76
+ - `toSequelizeFilter(ast)` – convert AST / QueryAST to a Sequelize‑style filter
77
+ - `toMongooseFilter(ast)` – convert AST / QueryAST to a Mongoose‑style filter
78
+ - `toKnexFilter(ast)` – convert AST / QueryAST to a Knex‑style filter
79
+ - `toDrizzleFilter(ast)` – convert AST / QueryAST to a Drizzle‑style filter
204
80
 
205
- // filter = {
206
- // __qast_logical__: 'and',
207
- // conditions: [
208
- // { age: { __qast_operator__: 'gt', value: 25 } },
209
- // { name: 'John' }
210
- // ]
211
- // }
81
+ All advanced utilities (caching, cost estimation, AST transforms, etc.) are kept small and dependency‑free.
212
82
 
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
- }
83
+ ---
238
84
 
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
85
+ ### License
763
86
 
764
87
  MIT © 2025
765
88
 
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.
89
+ GitHub: https://github.com/hocestnonsatis/qast