befly 3.9.12 → 3.9.13
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/docs/README.md +85 -0
- package/docs/addon.md +512 -0
- package/docs/api.md +1368 -0
- package/docs/cipher.md +580 -0
- package/docs/config.md +638 -0
- package/docs/examples.md +799 -0
- package/docs/hook.md +754 -0
- package/docs/logger.md +495 -0
- package/docs/plugin.md +978 -0
- package/docs/quickstart.md +331 -0
- package/docs/sync.md +586 -0
- package/docs/table.md +765 -0
- package/docs/validator.md +588 -0
- package/package.json +3 -3
- package/sync/syncDb/apply.ts +1 -1
- package/sync/syncDb/constants.ts +0 -11
- package/sync/syncDb/helpers.ts +0 -1
- package/sync/syncDb/table.ts +2 -2
- package/sync/syncDb/tableCreate.ts +3 -3
- package/tests/syncDb-constants.test.ts +1 -23
- package/tests/syncDb-helpers.test.ts +0 -1
- package/types/database.d.ts +0 -2
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
# Validator 参数验证
|
|
2
|
+
|
|
3
|
+
> 请求参数验证与类型转换
|
|
4
|
+
|
|
5
|
+
## 目录
|
|
6
|
+
|
|
7
|
+
- [概述](#概述)
|
|
8
|
+
- [验证器类](#验证器类)
|
|
9
|
+
- [验证规则](#验证规则)
|
|
10
|
+
- [类型转换](#类型转换)
|
|
11
|
+
- [正则别名](#正则别名)
|
|
12
|
+
- [验证钩子](#验证钩子)
|
|
13
|
+
- [API 字段定义](#api-字段定义)
|
|
14
|
+
- [验证结果](#验证结果)
|
|
15
|
+
- [使用示例](#使用示例)
|
|
16
|
+
- [FAQ](#faq)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 概述
|
|
21
|
+
|
|
22
|
+
Validator 是 Befly 的参数验证系统,提供:
|
|
23
|
+
|
|
24
|
+
- **数据验证**:根据字段定义验证数据
|
|
25
|
+
- **类型转换**:自动转换为目标类型
|
|
26
|
+
- **规则检查**:长度、范围、正则等
|
|
27
|
+
- **钩子集成**:自动验证 API 请求参数
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 验证器类
|
|
32
|
+
|
|
33
|
+
### 基本结构
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { Validator } from '../lib/validator.js';
|
|
37
|
+
|
|
38
|
+
class Validator {
|
|
39
|
+
// 验证数据对象
|
|
40
|
+
static validate(data: Record<string, any>, rules: TableDefinition, required: string[]): ValidateResult;
|
|
41
|
+
|
|
42
|
+
// 验证单个值(带类型转换)
|
|
43
|
+
static single(value: any, fieldDef: FieldDefinition): SingleResult;
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### validate 方法
|
|
48
|
+
|
|
49
|
+
批量验证数据对象:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
const data = {
|
|
53
|
+
email: 'test@example.com',
|
|
54
|
+
age: 25,
|
|
55
|
+
name: 'John'
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const rules = {
|
|
59
|
+
email: { name: '邮箱', type: 'string', min: 5, max: 100, regexp: '@email' },
|
|
60
|
+
age: { name: '年龄', type: 'number', min: 0, max: 150 },
|
|
61
|
+
name: { name: '姓名', type: 'string', min: 2, max: 50 }
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const result = Validator.validate(data, rules, ['email', 'name']);
|
|
65
|
+
|
|
66
|
+
if (result.failed) {
|
|
67
|
+
console.log(result.firstError); // 第一条错误信息
|
|
68
|
+
console.log(result.errors); // 所有错误信息
|
|
69
|
+
console.log(result.errorFields); // 出错字段列表
|
|
70
|
+
console.log(result.fieldErrors); // 字段->错误映射
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### single 方法
|
|
75
|
+
|
|
76
|
+
验证单个值并进行类型转换:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
const fieldDef = {
|
|
80
|
+
name: '年龄',
|
|
81
|
+
type: 'number',
|
|
82
|
+
min: 0,
|
|
83
|
+
max: 150,
|
|
84
|
+
default: 0
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const result = Validator.single('25', fieldDef);
|
|
88
|
+
|
|
89
|
+
if (!result.error) {
|
|
90
|
+
console.log(result.value); // 25 (已转换为 number)
|
|
91
|
+
} else {
|
|
92
|
+
console.log(result.error); // 错误信息
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 验证规则
|
|
99
|
+
|
|
100
|
+
### 字段定义格式
|
|
101
|
+
|
|
102
|
+
字段定义包含以下属性:
|
|
103
|
+
|
|
104
|
+
| 属性 | 类型 | 必填 | 说明 |
|
|
105
|
+
| ---------- | ------- | ---- | ------------------------ |
|
|
106
|
+
| `name` | string | 是 | 字段标签(用于错误提示) |
|
|
107
|
+
| `type` | string | 是 | 数据类型 |
|
|
108
|
+
| `min` | number | 否 | 最小值/长度 |
|
|
109
|
+
| `max` | number | 否 | 最大值/长度 |
|
|
110
|
+
| `default` | any | 否 | 默认值 |
|
|
111
|
+
| `regexp` | string | 否 | 正则表达式或别名 |
|
|
112
|
+
| `required` | boolean | 否 | 是否必填 |
|
|
113
|
+
|
|
114
|
+
### 支持的类型
|
|
115
|
+
|
|
116
|
+
| 类型 | 说明 | min/max 含义 |
|
|
117
|
+
| -------------- | ---------- | ------------ |
|
|
118
|
+
| `string` | 字符串 | 字符长度 |
|
|
119
|
+
| `text` | 长文本 | 字符长度 |
|
|
120
|
+
| `number` | 数字 | 数值范围 |
|
|
121
|
+
| `array_string` | 字符串数组 | 元素数量 |
|
|
122
|
+
| `array_text` | 文本数组 | 元素数量 |
|
|
123
|
+
|
|
124
|
+
### 验证逻辑
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
┌─────────────────────────────────────────────────────┐
|
|
128
|
+
│ 验证流程 │
|
|
129
|
+
├─────────────────────────────────────────────────────┤
|
|
130
|
+
│ 1. 参数检查 │
|
|
131
|
+
│ └── 确保 data 和 rules 是有效对象 │
|
|
132
|
+
│ ↓ │
|
|
133
|
+
│ 2. 必填字段检查 │
|
|
134
|
+
│ └── 检查 required 数组中的字段是否有值 │
|
|
135
|
+
│ ↓ │
|
|
136
|
+
│ 3. 类型转换 │
|
|
137
|
+
│ └── 将值转换为目标类型 │
|
|
138
|
+
│ ↓ │
|
|
139
|
+
│ 4. 规则验证 │
|
|
140
|
+
│ ├── 数字:检查 min/max 范围 │
|
|
141
|
+
│ ├── 字符串:检查长度 min/max │
|
|
142
|
+
│ ├── 数组:检查元素数量 │
|
|
143
|
+
│ └── 正则:检查格式是否匹配 │
|
|
144
|
+
│ ↓ │
|
|
145
|
+
│ 5. 构建结果 │
|
|
146
|
+
│ └── 返回 ValidateResult 对象 │
|
|
147
|
+
└─────────────────────────────────────────────────────┘
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 类型转换
|
|
153
|
+
|
|
154
|
+
### 转换规则
|
|
155
|
+
|
|
156
|
+
| 目标类型 | 输入 | 输出 |
|
|
157
|
+
| -------------- | ------------ | ------------ |
|
|
158
|
+
| `number` | `"123"` | `123` |
|
|
159
|
+
| `number` | `123` | `123` |
|
|
160
|
+
| `number` | `"abc"` | **错误** |
|
|
161
|
+
| `string` | `"hello"` | `"hello"` |
|
|
162
|
+
| `string` | `123` | **错误** |
|
|
163
|
+
| `array_string` | `["a", "b"]` | `["a", "b"]` |
|
|
164
|
+
| `array_string` | `"abc"` | **错误** |
|
|
165
|
+
|
|
166
|
+
### 空值处理
|
|
167
|
+
|
|
168
|
+
空值(`undefined`、`null`、`""`)时返回默认值:
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// 有 default 属性时使用 default
|
|
172
|
+
{ type: 'number', default: 10 } → 10
|
|
173
|
+
{ type: 'string', default: '默认' } → '默认'
|
|
174
|
+
|
|
175
|
+
// 无 default 时使用类型默认值
|
|
176
|
+
{ type: 'number' } → 0
|
|
177
|
+
{ type: 'string' } → ''
|
|
178
|
+
{ type: 'array_string' } → []
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### 数组默认值
|
|
182
|
+
|
|
183
|
+
数组类型的默认值可以是字符串格式:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
// 字符串格式的数组默认值
|
|
187
|
+
{ type: 'array_string', default: '[]' } → []
|
|
188
|
+
{ type: 'array_string', default: '["a","b"]' } → ['a', 'b']
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## 正则别名
|
|
194
|
+
|
|
195
|
+
### 使用方式
|
|
196
|
+
|
|
197
|
+
正则可以使用别名(以 `@` 开头)或直接写正则表达式:
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
// 使用别名
|
|
201
|
+
{
|
|
202
|
+
regexp: '@email';
|
|
203
|
+
} // 邮箱格式
|
|
204
|
+
{
|
|
205
|
+
regexp: '@phone';
|
|
206
|
+
} // 手机号格式
|
|
207
|
+
{
|
|
208
|
+
regexp: '@url';
|
|
209
|
+
} // URL 格式
|
|
210
|
+
|
|
211
|
+
// 直接使用正则
|
|
212
|
+
{
|
|
213
|
+
regexp: '^[a-z]+$';
|
|
214
|
+
} // 小写字母
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### 内置别名
|
|
218
|
+
|
|
219
|
+
| 别名 | 说明 | 正则 |
|
|
220
|
+
| ----------- | --------------- | ------------------------ | -------- | ----- |
|
|
221
|
+
| `@number` | 正整数 | `^\d+$` |
|
|
222
|
+
| `@integer` | 整数(含负数) | `^-?\d+$` |
|
|
223
|
+
| `@float` | 浮点数 | `^-?\d+(\.\d+)?$` |
|
|
224
|
+
| `@positive` | 正整数(不含0) | `^[1-9]\d*$` |
|
|
225
|
+
| `@email` | 邮箱 | `^[a-zA-Z0-9._%+-]+@...` |
|
|
226
|
+
| `@phone` | 手机号 | `^1[3-9]\d{9}$` |
|
|
227
|
+
| `@url` | URL | `^https?://` |
|
|
228
|
+
| `@ip` | IPv4 | `^((25[0-5] | 2[0-4]\d | ...)` |
|
|
229
|
+
| `@uuid` | UUID | `^[0-9a-f]{8}-...` |
|
|
230
|
+
| `@date` | 日期 | `^\d{4}-\d{2}-\d{2}$` |
|
|
231
|
+
| `@time` | 时间 | `^\d{2}:\d{2}:\d{2}$` |
|
|
232
|
+
| `@datetime` | 日期时间 | `^\d{4}-\d{2}-\d{2}T...` |
|
|
233
|
+
|
|
234
|
+
### 完整别名列表
|
|
235
|
+
|
|
236
|
+
**数字类**:
|
|
237
|
+
|
|
238
|
+
- `@number` - 正整数
|
|
239
|
+
- `@integer` - 整数(含负数)
|
|
240
|
+
- `@float` - 浮点数
|
|
241
|
+
- `@positive` - 正整数(不含0)
|
|
242
|
+
- `@negative` - 负整数
|
|
243
|
+
- `@zero` - 零
|
|
244
|
+
|
|
245
|
+
**字符类**:
|
|
246
|
+
|
|
247
|
+
- `@word` - 纯字母
|
|
248
|
+
- `@alphanumeric` - 字母和数字
|
|
249
|
+
- `@alphanumeric_` - 字母、数字和下划线
|
|
250
|
+
- `@lowercase` - 小写字母
|
|
251
|
+
- `@uppercase` - 大写字母
|
|
252
|
+
- `@chinese` - 纯中文
|
|
253
|
+
- `@chineseWord` - 中文和字母
|
|
254
|
+
|
|
255
|
+
**网络类**:
|
|
256
|
+
|
|
257
|
+
- `@email` - 邮箱
|
|
258
|
+
- `@url` - URL
|
|
259
|
+
- `@ip` - IPv4
|
|
260
|
+
- `@ipv6` - IPv6
|
|
261
|
+
- `@domain` - 域名
|
|
262
|
+
|
|
263
|
+
**编码类**:
|
|
264
|
+
|
|
265
|
+
- `@uuid` - UUID
|
|
266
|
+
- `@hex` - 十六进制
|
|
267
|
+
- `@base64` - Base64
|
|
268
|
+
- `@md5` - MD5
|
|
269
|
+
- `@sha1` - SHA1
|
|
270
|
+
- `@sha256` - SHA256
|
|
271
|
+
|
|
272
|
+
**日期时间**:
|
|
273
|
+
|
|
274
|
+
- `@date` - 日期 YYYY-MM-DD
|
|
275
|
+
- `@time` - 时间 HH:MM:SS
|
|
276
|
+
- `@datetime` - ISO 日期时间
|
|
277
|
+
- `@year` - 年份
|
|
278
|
+
- `@month` - 月份
|
|
279
|
+
- `@day` - 日期
|
|
280
|
+
|
|
281
|
+
**标识符**:
|
|
282
|
+
|
|
283
|
+
- `@variable` - 变量名
|
|
284
|
+
- `@constant` - 常量名
|
|
285
|
+
- `@package` - 包名
|
|
286
|
+
- `@username` - 用户名
|
|
287
|
+
- `@nickname` - 昵称
|
|
288
|
+
|
|
289
|
+
**账号类**:
|
|
290
|
+
|
|
291
|
+
- `@phone` - 手机号
|
|
292
|
+
- `@telephone` - 固定电话
|
|
293
|
+
- `@idCard` - 身份证号
|
|
294
|
+
- `@bankCard` - 银行卡号
|
|
295
|
+
- `@qq` - QQ号
|
|
296
|
+
- `@wechat` - 微信号
|
|
297
|
+
|
|
298
|
+
**密码类**:
|
|
299
|
+
|
|
300
|
+
- `@passwordWeak` - 弱密码(6位以上)
|
|
301
|
+
- `@passwordMedium` - 中等密码(8位,含字母数字)
|
|
302
|
+
- `@passwordStrong` - 强密码(8位,含大小写、数字、特殊字符)
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## 验证钩子
|
|
307
|
+
|
|
308
|
+
### 自动验证
|
|
309
|
+
|
|
310
|
+
Validator Hook 自动验证 API 请求参数:
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
// hooks/validator.ts
|
|
314
|
+
const hook: Hook = {
|
|
315
|
+
order: 6, // 在 parser 之后执行
|
|
316
|
+
handler: async (befly, ctx) => {
|
|
317
|
+
if (!ctx.api?.fields) return;
|
|
318
|
+
|
|
319
|
+
const result = Validator.validate(ctx.body, ctx.api.fields, ctx.api.required || []);
|
|
320
|
+
|
|
321
|
+
if (result.code !== 0) {
|
|
322
|
+
ctx.response = ErrorResponse(ctx, result.firstError || '参数验证失败', 1, null, result.fieldErrors);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### 执行顺序
|
|
329
|
+
|
|
330
|
+
```
|
|
331
|
+
请求 → parser (解析) → validator (验证) → API handler
|
|
332
|
+
↓
|
|
333
|
+
验证失败则返回错误响应
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## API 字段定义
|
|
339
|
+
|
|
340
|
+
### 在 API 中定义字段
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
// apis/user/login.ts
|
|
344
|
+
export default {
|
|
345
|
+
name: '用户登录',
|
|
346
|
+
method: 'POST',
|
|
347
|
+
auth: false,
|
|
348
|
+
fields: {
|
|
349
|
+
email: { name: '邮箱', type: 'string', min: 5, max: 100, regexp: '@email' },
|
|
350
|
+
password: { name: '密码', type: 'string', min: 6, max: 100 }
|
|
351
|
+
},
|
|
352
|
+
required: ['email', 'password'],
|
|
353
|
+
handler: async (befly, ctx) => {
|
|
354
|
+
// ctx.body.email 和 ctx.body.password 已验证
|
|
355
|
+
return Yes('登录成功');
|
|
356
|
+
}
|
|
357
|
+
} as ApiRoute;
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### 引用表字段
|
|
361
|
+
|
|
362
|
+
可以引用表定义中的字段:
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
import { adminTable } from '../../../tables/admin.js';
|
|
366
|
+
|
|
367
|
+
export default {
|
|
368
|
+
name: '创建管理员',
|
|
369
|
+
fields: {
|
|
370
|
+
email: adminTable.email, // 引用表字段
|
|
371
|
+
password: adminTable.password,
|
|
372
|
+
nickname: adminTable.nickname
|
|
373
|
+
},
|
|
374
|
+
required: ['email', 'password'],
|
|
375
|
+
handler: async (befly, ctx) => {
|
|
376
|
+
// ...
|
|
377
|
+
}
|
|
378
|
+
} as ApiRoute;
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### 使用公共字段
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
import { Fields } from '../../../config/fields.js';
|
|
385
|
+
|
|
386
|
+
export default {
|
|
387
|
+
name: '获取列表',
|
|
388
|
+
fields: {
|
|
389
|
+
...Fields.page, // 分页字段
|
|
390
|
+
...Fields.limit,
|
|
391
|
+
keyword: { name: '关键词', type: 'string', max: 50 }
|
|
392
|
+
},
|
|
393
|
+
handler: async (befly, ctx) => {
|
|
394
|
+
// ...
|
|
395
|
+
}
|
|
396
|
+
} as ApiRoute;
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## 验证结果
|
|
402
|
+
|
|
403
|
+
### ValidateResult 结构
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
interface ValidateResult {
|
|
407
|
+
code: number; // 0=成功,1=失败
|
|
408
|
+
failed: boolean; // 是否失败
|
|
409
|
+
firstError: string | null; // 第一条错误信息
|
|
410
|
+
errors: string[]; // 所有错误信息
|
|
411
|
+
errorFields: string[]; // 出错字段名列表
|
|
412
|
+
fieldErrors: Record<string, string>; // 字段->错误映射
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### SingleResult 结构
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
interface SingleResult {
|
|
420
|
+
value: any; // 转换后的值
|
|
421
|
+
error: string | null; // 错误信息
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### 结果示例
|
|
426
|
+
|
|
427
|
+
**验证成功**:
|
|
428
|
+
|
|
429
|
+
```typescript
|
|
430
|
+
{
|
|
431
|
+
code: 0,
|
|
432
|
+
failed: false,
|
|
433
|
+
firstError: null,
|
|
434
|
+
errors: [],
|
|
435
|
+
errorFields: [],
|
|
436
|
+
fieldErrors: {}
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**验证失败**:
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
443
|
+
{
|
|
444
|
+
code: 1,
|
|
445
|
+
failed: true,
|
|
446
|
+
firstError: '邮箱为必填项',
|
|
447
|
+
errors: ['邮箱为必填项', '密码长度不能少于6个字符'],
|
|
448
|
+
errorFields: ['email', 'password'],
|
|
449
|
+
fieldErrors: {
|
|
450
|
+
email: '邮箱为必填项',
|
|
451
|
+
password: '密码长度不能少于6个字符'
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## 使用示例
|
|
459
|
+
|
|
460
|
+
### 示例 1:基本验证
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
const data = {
|
|
464
|
+
username: 'john',
|
|
465
|
+
age: 25,
|
|
466
|
+
email: 'john@example.com'
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const rules = {
|
|
470
|
+
username: { name: '用户名', type: 'string', min: 2, max: 20 },
|
|
471
|
+
age: { name: '年龄', type: 'number', min: 0, max: 150 },
|
|
472
|
+
email: { name: '邮箱', type: 'string', regexp: '@email' }
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const result = Validator.validate(data, rules, ['username', 'email']);
|
|
476
|
+
// result.code === 0
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### 示例 2:类型转换
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
const data = {
|
|
483
|
+
age: '25', // 字符串
|
|
484
|
+
score: '98.5' // 字符串
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
const rules = {
|
|
488
|
+
age: { name: '年龄', type: 'number', min: 0 },
|
|
489
|
+
score: { name: '分数', type: 'number', min: 0, max: 100 }
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// 验证通过,'25' 会被转换为 25
|
|
493
|
+
const result = Validator.validate(data, rules);
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### 示例 3:数组验证
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
const data = {
|
|
500
|
+
tags: ['vue', 'react', 'angular'],
|
|
501
|
+
ids: [1, 2, 3]
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
const rules = {
|
|
505
|
+
tags: { name: '标签', type: 'array_string', min: 1, max: 10 },
|
|
506
|
+
ids: { name: 'ID列表', type: 'array_string', min: 1 }
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
const result = Validator.validate(data, rules, ['tags']);
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### 示例 4:正则验证
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
const data = {
|
|
516
|
+
phone: '13812345678',
|
|
517
|
+
email: 'test@example.com',
|
|
518
|
+
code: 'ABC123'
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const rules = {
|
|
522
|
+
phone: { name: '手机号', type: 'string', regexp: '@phone' },
|
|
523
|
+
email: { name: '邮箱', type: 'string', regexp: '@email' },
|
|
524
|
+
code: { name: '验证码', type: 'string', regexp: '^[A-Z0-9]{6}$' }
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const result = Validator.validate(data, rules);
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### 示例 5:单值验证
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
// 验证并转换单个值
|
|
534
|
+
const ageResult = Validator.single('25', {
|
|
535
|
+
name: '年龄',
|
|
536
|
+
type: 'number',
|
|
537
|
+
min: 0,
|
|
538
|
+
max: 150
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
if (!ageResult.error) {
|
|
542
|
+
console.log(ageResult.value); // 25 (number)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// 空值使用默认值
|
|
546
|
+
const emptyResult = Validator.single('', {
|
|
547
|
+
name: '数量',
|
|
548
|
+
type: 'number',
|
|
549
|
+
default: 10
|
|
550
|
+
});
|
|
551
|
+
console.log(emptyResult.value); // 10
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
---
|
|
555
|
+
|
|
556
|
+
## FAQ
|
|
557
|
+
|
|
558
|
+
### Q: 如何自定义错误信息?
|
|
559
|
+
|
|
560
|
+
A: 目前错误信息由验证器自动生成,格式为 `{字段标签}{错误描述}`。可以在 API handler 中捕获验证结果后自定义处理。
|
|
561
|
+
|
|
562
|
+
### Q: 如何跳过某些字段的验证?
|
|
563
|
+
|
|
564
|
+
A: 不在 `fields` 中定义的字段不会被验证。如果字段不在 `required` 数组中且值为空,也会跳过验证。
|
|
565
|
+
|
|
566
|
+
### Q: 验证失败后请求参数还能用吗?
|
|
567
|
+
|
|
568
|
+
A: 验证失败时 Hook 会直接返回错误响应,不会执行 API handler。如果需要在 handler 中手动验证,可以禁用 validator hook。
|
|
569
|
+
|
|
570
|
+
### Q: 如何验证嵌套对象?
|
|
571
|
+
|
|
572
|
+
A: 目前只支持扁平对象验证。嵌套对象需要在 handler 中手动验证。
|
|
573
|
+
|
|
574
|
+
### Q: 正则别名可以扩展吗?
|
|
575
|
+
|
|
576
|
+
A: 正则别名定义在 `befly-shared/regex` 中,可以直接使用自定义正则字符串,不需要扩展别名。
|
|
577
|
+
|
|
578
|
+
### Q: 类型转换失败会怎样?
|
|
579
|
+
|
|
580
|
+
A: 类型转换失败会在 `errors` 中记录错误,如 "年龄必须是数字"。
|
|
581
|
+
|
|
582
|
+
### Q: 数组元素如何验证?
|
|
583
|
+
|
|
584
|
+
A: 数组类型会验证:
|
|
585
|
+
|
|
586
|
+
1. 值是否为数组
|
|
587
|
+
2. 元素数量是否在 min/max 范围内
|
|
588
|
+
3. 如果有 regexp,每个元素都会进行正则验证
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "befly",
|
|
3
|
-
"version": "3.9.
|
|
3
|
+
"version": "3.9.13",
|
|
4
4
|
"description": "Befly - 为 Bun 专属打造的 TypeScript API 接口框架核心引擎",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"bun": ">=1.3.0"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"befly-shared": "^1.2.
|
|
68
|
+
"befly-shared": "^1.2.3",
|
|
69
69
|
"chalk": "^5.6.2",
|
|
70
70
|
"es-toolkit": "^1.42.0",
|
|
71
71
|
"fast-jwt": "^6.1.0",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"pino": "^10.1.0",
|
|
75
75
|
"pino-roll": "^4.0.0"
|
|
76
76
|
},
|
|
77
|
-
"gitHead": "
|
|
77
|
+
"gitHead": "f0dc38476a87163a9de1d5ddcefa42304b1ee09b",
|
|
78
78
|
"devDependencies": {
|
|
79
79
|
"typescript": "^5.9.3"
|
|
80
80
|
}
|
package/sync/syncDb/apply.ts
CHANGED
|
@@ -61,7 +61,7 @@ export function compareFieldDefinition(existingColumn: ColumnInfo, fieldDef: Fie
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
// 检查注释变化(MySQL/PG
|
|
64
|
+
// 检查注释变化(MySQL/PG 支持列注释,对比数据库 comment 与字段 name)
|
|
65
65
|
if (!isSQLite()) {
|
|
66
66
|
const currentComment = existingColumn.comment || '';
|
|
67
67
|
if (currentComment !== fieldDef.name) {
|
package/sync/syncDb/constants.ts
CHANGED
|
@@ -18,17 +18,6 @@ export const DB_VERSION_REQUIREMENTS = {
|
|
|
18
18
|
SQLITE_MIN_VERSION_NUM: 35000 // 3 * 10000 + 50 * 100
|
|
19
19
|
} as const;
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
* 系统字段定义(所有表都包含的固定字段)
|
|
23
|
-
*/
|
|
24
|
-
export const SYSTEM_FIELDS = {
|
|
25
|
-
ID: { name: 'id', comment: '主键ID' },
|
|
26
|
-
CREATED_AT: { name: 'created_at', comment: '创建时间' },
|
|
27
|
-
UPDATED_AT: { name: 'updated_at', comment: '更新时间' },
|
|
28
|
-
DELETED_AT: { name: 'deleted_at', comment: '删除时间' },
|
|
29
|
-
STATE: { name: 'state', comment: '状态字段' }
|
|
30
|
-
} as const;
|
|
31
|
-
|
|
32
21
|
/**
|
|
33
22
|
* 需要创建索引的系统字段
|
|
34
23
|
*/
|
package/sync/syncDb/helpers.ts
CHANGED
|
@@ -81,7 +81,6 @@ export function applyFieldDefaults(fieldDef: any): void {
|
|
|
81
81
|
fieldDef.default = fieldDef.default ?? null;
|
|
82
82
|
fieldDef.index = fieldDef.index ?? false;
|
|
83
83
|
fieldDef.unique = fieldDef.unique ?? false;
|
|
84
|
-
fieldDef.comment = fieldDef.comment ?? '';
|
|
85
84
|
fieldDef.nullable = fieldDef.nullable ?? false;
|
|
86
85
|
fieldDef.unsigned = fieldDef.unsigned ?? true;
|
|
87
86
|
fieldDef.regexp = fieldDef.regexp ?? null;
|
package/sync/syncDb/table.ts
CHANGED
|
@@ -170,8 +170,8 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
|
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
// PG
|
|
174
|
-
const commentActions = [];
|
|
173
|
+
// PG 列注释处理(对比数据库 comment 与字段 name)
|
|
174
|
+
const commentActions: string[] = [];
|
|
175
175
|
if (isPG()) {
|
|
176
176
|
for (const [fieldKey, fieldDef] of Object.entries(fields)) {
|
|
177
177
|
// 转换字段名为下划线格式
|
|
@@ -19,7 +19,7 @@ import type { SQL } from 'bun';
|
|
|
19
19
|
import type { FieldDefinition } from 'befly-shared/types';
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* 为 PostgreSQL
|
|
22
|
+
* 为 PostgreSQL 表添加列注释(使用字段的 name 作为注释)
|
|
23
23
|
*
|
|
24
24
|
* @param sql - SQL 客户端实例
|
|
25
25
|
* @param tableName - 表名
|
|
@@ -44,7 +44,7 @@ async function addPostgresComments(sql: SQL, tableName: string, fields: Record<s
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
//
|
|
47
|
+
// 业务字段注释(使用 fieldDef.name 作为数据库列注释)
|
|
48
48
|
for (const [fieldKey, fieldDef] of Object.entries(fields)) {
|
|
49
49
|
// 转换字段名为下划线格式
|
|
50
50
|
const dbFieldName = snakeCase(fieldKey);
|
|
@@ -149,7 +149,7 @@ export async function createTable(sql: SQL, tableName: string, fields: Record<st
|
|
|
149
149
|
await sql.unsafe(createSQL);
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
// PostgreSQL:
|
|
152
|
+
// PostgreSQL: 添加列注释(使用字段 name 作为数据库列注释)
|
|
153
153
|
if (isPG() && !IS_PLAN) {
|
|
154
154
|
await addPostgresComments(sql, tableName, fields);
|
|
155
155
|
} else if (isPG() && IS_PLAN) {
|