qast 2.0.3 → 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.zh-CN.md DELETED
@@ -1,520 +0,0 @@
1
- # QAST — 查询字符串转AST转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** 是一个轻量级、ORM无关的库,它可以将人类可读的查询字符串(例如 `age gt 25 and (name eq "John" or city eq "Paris")`)解析为**抽象语法树(AST)**,然后将该AST转换为**ORM兼容的过滤对象**,如Prisma或TypeORM过滤器。
10
-
11
- 它旨在为REST API提供一种安全、声明式且类型安全的高级过滤支持方式——避免原始字符串查询模式的陷阱。
12
-
13
- ## 特性
14
-
15
- - 🔒 **安全**:根据白名单验证操作符、值和字段
16
- - 🎯 **类型安全**:完整的TypeScript支持,包括解析后的AST和生成的过滤器
17
- - 🔌 **ORM无关**:通过适配器支持Prisma、TypeORM、Sequelize等
18
- - 📝 **简单语法**:使用逻辑操作符的自然查询表达式
19
- - 🚀 **轻量级**:无依赖,包体积小
20
-
21
- ## 安装
22
-
23
- ```bash
24
- npm install qast
25
- ```
26
-
27
- ## 快速开始
28
-
29
- ### 基本用法
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
- ### 带验证的用法
43
-
44
- ```typescript
45
- import { parseQuery, toPrismaFilter } from 'qast';
46
-
47
- const query = 'age gt 25 and name eq "John"';
48
-
49
- // 使用白名单验证进行解析
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
- ## 查询语法
61
-
62
- ### 操作符
63
-
64
- QAST支持以下比较操作符:
65
-
66
- - `eq` - 等于
67
- - `ne` - 不等于
68
- - `gt` - 大于
69
- - `lt` - 小于
70
- - `gte` - 大于等于
71
- - `lte` - 小于等于
72
- - `in` - 在数组中
73
- - `contains` - 包含子字符串(字符串匹配)
74
-
75
- ### 逻辑操作符
76
-
77
- - `and` - 逻辑与
78
- - `or` - 逻辑或
79
-
80
- ### 值类型
81
-
82
- - **字符串**:使用单引号或双引号:`"John"` 或 `'John'`
83
- - **数字**:整数或浮点数:`25`、`25.99`、`-10`
84
- - **布尔值**:`true` 或 `false`
85
- - **数组**:用于 `in` 操作符:`[1,2,3]` 或 `["John","Jane"]`
86
-
87
- ### 示例
88
-
89
- ```typescript
90
- // 简单比较
91
- 'age gt 25'
92
-
93
- // 字符串比较
94
- 'name eq "John"'
95
-
96
- // 布尔值比较
97
- 'active eq true'
98
-
99
- // 数组(in操作符)
100
- 'age in [1,2,3]'
101
-
102
- // AND操作
103
- 'age gt 25 and name eq "John"'
104
-
105
- // OR操作
106
- 'name eq "John" or name eq "Jane"'
107
-
108
- // 嵌套括号
109
- 'age gt 25 and (name eq "John" or city eq "Paris")'
110
-
111
- // 复杂查询
112
- 'age gt 25 and (name eq "John" or city eq "Paris") and active eq true'
113
- ```
114
-
115
- ## ORM适配器
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
- // 注意:TypeORM需要操作符函数来进行非等值比较
147
- // 适配器返回一个结构,您可以使用TypeORM操作符进行转换
148
- // 对于等值比较,TypeORM直接接受普通值
149
-
150
- // filter.where = {
151
- // age: { __qast_operator__: 'gt', value: 25 },
152
- // name: "John"
153
- // }
154
-
155
- // 转换为使用TypeORM操作符:
156
- // const transformed = {
157
- // age: MoreThan(25),
158
- // name: "John"
159
- // }
160
-
161
- await userRepository.find({ where: transformed });
162
- ```
163
-
164
- **注意**:TypeORM需要操作符函数(`MoreThan`、`LessThan`等)来进行非等值比较。适配器返回带有元数据的结构,您需要进行转换。对于等值比较,TypeORM直接接受普通值。
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
- // 转换为使用Sequelize Op操作符:
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
- **注意**:Sequelize使用来自'sequelize'的`Op`对象。由于Sequelize是可选的对等依赖,适配器返回带有元数据(`__qast_operator__`和`__qast_logical__`)的结构,您需要将其转换为使用`Op`操作符。对于简单的等值比较(`eq`),适配器返回Sequelize直接接受的普通值。
222
-
223
- ## API参考
224
-
225
- ### `parseQuery(query: string, options?: ParseOptions): QastNode`
226
-
227
- 将查询字符串解析为AST。
228
-
229
- **参数:**
230
- - `query` - 要解析的查询字符串
231
- - `options` - 可选的解析选项:
232
- - `allowedFields?: string[]` - 允许的字段名白名单
233
- - `allowedOperators?: Operator[]` - 允许的操作符白名单
234
- - `validate?: boolean` - 是否根据白名单进行验证(如果提供了白名单,默认为true)
235
- - `maxDepth?: number` - 最大允许的AST深度(用于限制嵌套逻辑表达式)
236
- - `maxNodes?: number` - 最大允许的AST节点数(用于限制整体查询复杂度)
237
- - `maxQueryLength?: number` - 最大允许的原始查询字符串长度(在解析前检查)
238
- - `maxArrayLength?: number` - 最大允许的数组值长度(用于`in`操作符)
239
- - `maxStringLength?: number` - 最大允许的字符串值长度
240
-
241
- **返回值:** 解析后的AST节点
242
-
243
- **示例:**
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
- 将AST转换为Prisma过滤器。
260
-
261
- **返回值:** 带有`where`属性的Prisma过滤对象
262
-
263
- ### `toTypeORMFilter(ast: QastNode): TypeORMFilter`
264
-
265
- 将AST转换为TypeORM过滤器。
266
-
267
- **返回值:** 带有`where`属性的TypeORM过滤对象
268
-
269
- **注意:** TypeORM需要操作符函数来进行非等值比较。您可能需要转换结果。
270
-
271
- ### `toSequelizeFilter(ast: QastNode): SequelizeFilter`
272
-
273
- 将AST转换为Sequelize过滤器。
274
-
275
- **返回值:** Sequelize过滤对象
276
-
277
- **注意:** Sequelize使用`Op`对象。您需要将`$`前缀的操作符转换为使用`Op`操作符。
278
-
279
- ### `validateQuery(ast: QastNode, whitelist: WhitelistOptions): void`
280
-
281
- 根据白名单验证AST。
282
-
283
- **参数:**
284
- - `ast` - 要验证的AST
285
- - `whitelist` - 白名单选项:
286
- - `allowedFields?: string[]` - 允许的字段名
287
- - `allowedOperators?: Operator[]` - 允许的操作符
288
-
289
- **抛出:** 如果验证失败,抛出`ValidationError`
290
-
291
- ### `extractFields(ast: QastNode): string[]`
292
-
293
- 提取AST中使用的所有字段名。
294
-
295
- **返回值:** 唯一字段名数组
296
-
297
- ### `extractOperators(ast: QastNode): Operator[]`
298
-
299
- 提取AST中使用的所有操作符。
300
-
301
- **返回值:** 唯一操作符数组
302
-
303
- ### `validateQueryComplexity(ast: QastNode, options: ComplexityOptions): void`
304
-
305
- 根据复杂度限制验证AST。
306
-
307
- **参数:**
308
- - `ast` - 要验证的AST
309
- - `options` - 复杂度选项:
310
- - `maxDepth?: number` - 最大允许的AST深度
311
- - `maxNodes?: number` - 最大允许的节点数
312
- - `maxArrayLength?: number` - 最大允许的数组长度
313
- - `maxStringLength?: number` - 最大允许的字符串长度
314
-
315
- **抛出:** 如果超出任何限制,抛出`ValidationError`
316
-
317
- **示例:**
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
- ## 安全最佳实践
331
-
332
- 1. **始终使用白名单**:限制查询中可以使用的字段和操作符。
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. **验证用户输入**:不要在没有验证的情况下信任用户提供的查询字符串。
343
-
344
- 3. **限制查询复杂度**:使用复杂度限制来防止DoS攻击。
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
- // 复杂度限制
352
- maxQueryLength: 1000, // 拒绝超过1000字符的查询
353
- maxDepth: 5, // 最多5层嵌套
354
- maxNodes: 20, // 最多20个条件
355
- maxArrayLength: 100, // 'in'数组最多100项
356
- maxStringLength: 200, // 每个字符串值最多200字符
357
- });
358
- ```
359
-
360
- 4. **使用类型检查**:确保值与字段的预期类型匹配。
361
-
362
- ## 错误处理
363
-
364
- QAST提供自定义错误类:
365
-
366
- - `ParseError` - 查询字符串中的语法错误
367
- - `ValidationError` - 验证失败(不允许的字段/操作符)
368
- - `TokenizationError` - 词法分析错误
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('解析错误:', error.message);
378
- } else if (error instanceof ValidationError) {
379
- console.error('验证错误:', error.message);
380
- console.error('字段:', error.field);
381
- console.error('操作符:', error.operator);
382
- }
383
- }
384
- ```
385
-
386
- ## TypeScript支持
387
-
388
- QAST使用TypeScript编写,并提供完整的类型定义:
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
- ## 示例
406
-
407
- ### REST API端点
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
- // 解析和验证查询
420
- const ast = parseQuery(query, {
421
- allowedFields: ['age', 'name', 'city', 'active'],
422
- allowedOperators: ['gt', 'lt', 'eq', 'in'],
423
- validate: true,
424
- });
425
-
426
- // 转换为Prisma过滤器
427
- const filter = toPrismaFilter(ast);
428
-
429
- // 查询数据库
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: '内部服务器错误' });
438
- }
439
- }
440
- });
441
- ```
442
-
443
- ### Express中间件
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
- ## 与替代方案的比较
473
-
474
- ### 为什么选择QAST?
475
-
476
- | 特性 | QAST | GraphQL | OData | 自定义解析器 |
477
- |------|------|---------|-------|-------------|
478
- | **类型安全** | ✅ 完整TypeScript | ❌ 仅运行时 | ⚠️ 部分 | ❌ 通常没有 |
479
- | **安全性** | ✅ 白名单验证 | ✅ 内置 | ✅ 内置 | ⚠️ 手动 |
480
- | **ORM无关** | ✅ 是 | ❌ 否 | ❌ 否 | ⚠️ 因情况而异 |
481
- | **零依赖** | ✅ 是 | ❌ 否 | ❌ 否 | ⚠️ 因情况而异 |
482
- | **学习曲线** | ✅ 简单 | ❌ 复杂 | ❌ 复杂 | ⚠️ 因情况而异 |
483
- | **REST API友好** | ✅ 是 | ❌ 需要GraphQL端点 | ✅ 是 | ⚠️ 因情况而异 |
484
- | **包大小** | ✅ < 10KB | ❌ 大 | ❌ 大 | ⚠️ 因情况而异 |
485
-
486
- **适合使用QAST的场景:**
487
- - 您想要一个简单、安全的REST API查询语言
488
- - 您需要使用TypeScript进行类型安全的查询解析
489
- - 您正在使用Prisma、TypeORM或Sequelize
490
- - 您想要零依赖和小的包大小
491
- - 您需要字段和操作符白名单以确保安全
492
-
493
- **考虑替代方案的场景:**
494
- - 您需要GraphQL的完整查询能力
495
- - 您需要标准化的查询协议(OData)
496
- - 您有复杂的嵌套数据关系
497
-
498
- ## 示例项目
499
-
500
- 查看[`examples`](./examples)目录获取完整的工作示例:
501
-
502
- - [Express.js REST API](./examples/express) - 完整的Express.js服务器与Prisma
503
- - [Next.js API路由](./examples/nextjs) - 使用QAST的Next.js API路由
504
- - [NestJS集成](./examples/nestjs) - 带有查询过滤的NestJS控制器
505
- - [交互式演示](./examples/playground.html) - 在浏览器中尝试QAST查询(演示)
506
-
507
- ## 许可证
508
-
509
- MIT © 2025
510
-
511
- ## 贡献
512
-
513
- 欢迎贡献!详情请参阅[CONTRIBUTING.md](./CONTRIBUTING.md)。
514
-
515
- - GitHub仓库:https://github.com/hocestnonsatis/qast
516
- - 问题反馈:https://github.com/hocestnonsatis/qast/issues
517
-
518
- ## 致谢
519
-
520
- QAST的灵感来源于REST API中对安全、类型安全的查询解析的需求。它旨在提供一个轻量级的替代方案,同时保持安全性和开发者体验。