befly 3.9.13 → 3.9.21
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/checks/checkTable.ts +18 -0
- package/docs/api.md +247 -11
- package/docs/database.md +147 -3
- package/docs/examples.md +103 -10
- package/docs/validator.md +30 -0
- package/loader/loadApis.ts +33 -36
- package/package.json +2 -2
- package/sync/syncDb/table.ts +2 -1
package/checks/checkTable.ts
CHANGED
|
@@ -37,6 +37,11 @@ const RESERVED_FIELDS = ['id', 'created_at', 'updated_at', 'deleted_at', 'state'
|
|
|
37
37
|
*/
|
|
38
38
|
const FIELD_TYPES = ['string', 'number', 'text', 'array_string', 'array_text'] as const;
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* 允许的字段属性列表
|
|
42
|
+
*/
|
|
43
|
+
const ALLOWED_FIELD_PROPERTIES = ['name', 'type', 'min', 'max', 'default', 'detail', 'index', 'unique', 'nullable', 'unsigned', 'regexp'] as const;
|
|
44
|
+
|
|
40
45
|
/**
|
|
41
46
|
* 小驼峰命名正则
|
|
42
47
|
* 可选:以下划线开头(用于特殊文件,如通用字段定义)
|
|
@@ -133,6 +138,14 @@ export async function checkTable(): Promise<void> {
|
|
|
133
138
|
// 直接使用字段对象
|
|
134
139
|
const field = fieldDef as FieldDefinition;
|
|
135
140
|
|
|
141
|
+
// 检查是否存在非法属性
|
|
142
|
+
const fieldKeys = Object.keys(field);
|
|
143
|
+
const illegalProps = fieldKeys.filter((key) => !ALLOWED_FIELD_PROPERTIES.includes(key as any));
|
|
144
|
+
if (illegalProps.length > 0) {
|
|
145
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 包含非法属性: ${illegalProps.join(', ')},` + `允许的属性为: ${ALLOWED_FIELD_PROPERTIES.join(', ')}`);
|
|
146
|
+
hasError = true;
|
|
147
|
+
}
|
|
148
|
+
|
|
136
149
|
// 检查必填字段:name, type
|
|
137
150
|
if (!field.name || typeof field.name !== 'string') {
|
|
138
151
|
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 缺少必填字段 name 或类型错误`);
|
|
@@ -193,6 +206,11 @@ export async function checkTable(): Promise<void> {
|
|
|
193
206
|
hasError = true;
|
|
194
207
|
}
|
|
195
208
|
|
|
209
|
+
// 检查 unique 和 index 冲突(警告但不阻断)
|
|
210
|
+
if (field.unique && field.index) {
|
|
211
|
+
Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 同时设置了 unique=true 和 index=true,` + `unique 约束会自动创建唯一索引,index=true 将被忽略以避免重复索引`);
|
|
212
|
+
}
|
|
213
|
+
|
|
196
214
|
// 约束:当最小值与最大值均为数字时,要求最小值 <= 最大值
|
|
197
215
|
if (fieldMin !== undefined && fieldMax !== undefined && fieldMin !== null && fieldMax !== null) {
|
|
198
216
|
if (fieldMin > fieldMax) {
|
package/docs/api.md
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
- [ErrorResponse - Hook 中断响应](#errorresponse---hook-中断响应)
|
|
25
25
|
- [FinalResponse - 最终响应](#finalresponse---最终响应)
|
|
26
26
|
- [字段定义与验证](#字段定义与验证)
|
|
27
|
-
- [
|
|
27
|
+
- [预定义字段](#预定义字段)
|
|
28
28
|
- [字段定义格式](#字段定义格式)
|
|
29
29
|
- [字段类型](#字段类型)
|
|
30
30
|
- [验证规则](#验证规则)
|
|
@@ -308,37 +308,39 @@ ctx.response = ErrorResponse(ctx, '未授权', 1, null);
|
|
|
308
308
|
|
|
309
309
|
## 字段定义与验证
|
|
310
310
|
|
|
311
|
-
###
|
|
311
|
+
### 预定义字段
|
|
312
312
|
|
|
313
|
-
|
|
313
|
+
框架提供了一套预定义字段系统,通过 `@` 符号引用常用字段,避免重复定义。
|
|
314
|
+
|
|
315
|
+
#### 可用预定义字段
|
|
314
316
|
|
|
315
317
|
```typescript
|
|
316
|
-
const
|
|
317
|
-
id: {
|
|
318
|
+
const PRESET_FIELDS = {
|
|
319
|
+
'@id': {
|
|
318
320
|
name: 'ID',
|
|
319
321
|
type: 'number',
|
|
320
322
|
min: 1,
|
|
321
323
|
max: null
|
|
322
324
|
},
|
|
323
|
-
page: {
|
|
325
|
+
'@page': {
|
|
324
326
|
name: '页码',
|
|
325
327
|
type: 'number',
|
|
326
328
|
min: 1,
|
|
327
329
|
max: 9999
|
|
328
330
|
},
|
|
329
|
-
limit: {
|
|
331
|
+
'@limit': {
|
|
330
332
|
name: '每页数量',
|
|
331
333
|
type: 'number',
|
|
332
334
|
min: 1,
|
|
333
335
|
max: 100
|
|
334
336
|
},
|
|
335
|
-
keyword: {
|
|
337
|
+
'@keyword': {
|
|
336
338
|
name: '关键词',
|
|
337
339
|
type: 'string',
|
|
338
340
|
min: 1,
|
|
339
341
|
max: 50
|
|
340
342
|
},
|
|
341
|
-
state: {
|
|
343
|
+
'@state': {
|
|
342
344
|
name: '状态',
|
|
343
345
|
type: 'number',
|
|
344
346
|
min: 0,
|
|
@@ -347,6 +349,192 @@ const DEFAULT_API_FIELDS = {
|
|
|
347
349
|
};
|
|
348
350
|
```
|
|
349
351
|
|
|
352
|
+
#### 预定义字段说明
|
|
353
|
+
|
|
354
|
+
| 字段 | 类型 | 范围 | 说明 |
|
|
355
|
+
| ---------- | -------- | --------- | ------------------------------------ |
|
|
356
|
+
| `@id` | `number` | >= 1 | 通用 ID 字段,用于详情/删除等 |
|
|
357
|
+
| `@page` | `number` | 1-9999 | 分页页码,默认从 1 开始 |
|
|
358
|
+
| `@limit` | `number` | 1-100 | 每页数量,最大 100 条 |
|
|
359
|
+
| `@keyword` | `string` | 1-50 字符 | 搜索关键词 |
|
|
360
|
+
| `@state` | `number` | 0-2 | 状态字段(0=软删除,1=正常,2=禁用) |
|
|
361
|
+
|
|
362
|
+
#### 使用方式
|
|
363
|
+
|
|
364
|
+
在 `fields` 中使用 `@` 符号引用预定义字段:
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
// 方式一:直接字符串引用
|
|
368
|
+
fields: {
|
|
369
|
+
id: '@id',
|
|
370
|
+
page: '@page',
|
|
371
|
+
limit: '@limit',
|
|
372
|
+
keyword: '@keyword',
|
|
373
|
+
state: '@state'
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// 方式二:与自定义字段混用
|
|
377
|
+
fields: {
|
|
378
|
+
page: '@page',
|
|
379
|
+
limit: '@limit',
|
|
380
|
+
categoryId: { name: '分类ID', type: 'number', min: 0 }
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
#### 加载机制
|
|
385
|
+
|
|
386
|
+
1. **按需引用**:只有在 `fields` 中显式声明的预定义字段才会生效
|
|
387
|
+
2. **自动替换**:在 API 加载时,`@` 引用会被自动替换为完整的字段定义
|
|
388
|
+
3. **验证生效**:引用的预定义字段会自动应用验证规则
|
|
389
|
+
|
|
390
|
+
#### 使用预定义字段示例
|
|
391
|
+
|
|
392
|
+
**列表查询接口**
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
// apis/article/list.ts
|
|
396
|
+
export default {
|
|
397
|
+
name: '文章列表',
|
|
398
|
+
auth: true,
|
|
399
|
+
fields: {
|
|
400
|
+
page: '@page',
|
|
401
|
+
limit: '@limit',
|
|
402
|
+
keyword: '@keyword',
|
|
403
|
+
state: '@state',
|
|
404
|
+
categoryId: { name: '分类ID', type: 'number', min: 0 }
|
|
405
|
+
},
|
|
406
|
+
handler: async (befly, ctx) => {
|
|
407
|
+
const { page, limit, keyword, categoryId } = ctx.body;
|
|
408
|
+
|
|
409
|
+
const where: Record<string, any> = { state: 1 };
|
|
410
|
+
if (categoryId) where.categoryId = categoryId;
|
|
411
|
+
if (keyword) where.title = { $like: `%${keyword}%` };
|
|
412
|
+
|
|
413
|
+
const result = await befly.db.getList({
|
|
414
|
+
table: 'article',
|
|
415
|
+
columns: ['id', 'title', 'summary', 'createdAt'],
|
|
416
|
+
where: where,
|
|
417
|
+
page: page || 1,
|
|
418
|
+
limit: limit || 10,
|
|
419
|
+
orderBy: { id: 'desc' }
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
return befly.tool.Yes('获取成功', result);
|
|
423
|
+
}
|
|
424
|
+
} as ApiRoute;
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
**详情/删除接口**
|
|
428
|
+
|
|
429
|
+
```typescript
|
|
430
|
+
// apis/article/detail.ts
|
|
431
|
+
export default {
|
|
432
|
+
name: '文章详情',
|
|
433
|
+
auth: false,
|
|
434
|
+
fields: {
|
|
435
|
+
id: '@id'
|
|
436
|
+
},
|
|
437
|
+
required: ['id'],
|
|
438
|
+
handler: async (befly, ctx) => {
|
|
439
|
+
const article = await befly.db.getDetail({
|
|
440
|
+
table: 'article',
|
|
441
|
+
where: { id: ctx.body.id, state: 1 }
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
if (!article?.id) {
|
|
445
|
+
return befly.tool.No('文章不存在');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return befly.tool.Yes('获取成功', article);
|
|
449
|
+
}
|
|
450
|
+
} as ApiRoute;
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
// apis/article/delete.ts
|
|
455
|
+
export default {
|
|
456
|
+
name: '删除文章',
|
|
457
|
+
auth: true,
|
|
458
|
+
fields: {
|
|
459
|
+
id: '@id'
|
|
460
|
+
},
|
|
461
|
+
required: ['id'],
|
|
462
|
+
handler: async (befly, ctx) => {
|
|
463
|
+
await befly.db.delData({
|
|
464
|
+
table: 'article',
|
|
465
|
+
where: { id: ctx.body.id }
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
return befly.tool.Yes('删除成功');
|
|
469
|
+
}
|
|
470
|
+
} as ApiRoute;
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
#### 覆盖预定义字段
|
|
474
|
+
|
|
475
|
+
如需修改预定义字段的验证规则,在 `fields` 中重新定义即可:
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
export default {
|
|
479
|
+
name: '大数据列表',
|
|
480
|
+
fields: {
|
|
481
|
+
page: '@page',
|
|
482
|
+
// 覆盖默认的 @limit,允许更大的分页
|
|
483
|
+
limit: {
|
|
484
|
+
name: '每页数量',
|
|
485
|
+
type: 'number',
|
|
486
|
+
min: 1,
|
|
487
|
+
max: 500 // 修改最大值为 500
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
handler: async (befly, ctx) => {
|
|
491
|
+
// ctx.body.limit 最大可以是 500
|
|
492
|
+
return befly.tool.Yes('获取成功');
|
|
493
|
+
}
|
|
494
|
+
} as ApiRoute;
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
#### 预定义字段最佳实践
|
|
498
|
+
|
|
499
|
+
**推荐使用场景**
|
|
500
|
+
|
|
501
|
+
| API 类型 | 推荐字段 | 说明 |
|
|
502
|
+
| -------- | ----------------------------------- | ------------------ |
|
|
503
|
+
| 列表查询 | `page`, `limit`, `keyword`, `state` | 完整的查询字段组合 |
|
|
504
|
+
| 获取详情 | `id` | 只需 ID 参数 |
|
|
505
|
+
| 删除操作 | `id` | 只需 ID 参数 |
|
|
506
|
+
| 更新操作 | `id` + 表字段 | ID + 业务字段 |
|
|
507
|
+
| 添加操作 | 表字段(无需预定义字段) | 只需业务字段 |
|
|
508
|
+
|
|
509
|
+
**使用建议**
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
// ✅ 推荐:列表查询使用完整预定义字段
|
|
513
|
+
fields: {
|
|
514
|
+
page: '@page',
|
|
515
|
+
limit: '@limit',
|
|
516
|
+
keyword: '@keyword',
|
|
517
|
+
state: '@state'
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ✅ 推荐:详情/删除只使用 id
|
|
521
|
+
fields: {
|
|
522
|
+
id: '@id'
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// ✅ 推荐:更新接口混用预定义和表字段
|
|
526
|
+
fields: {
|
|
527
|
+
id: '@id',
|
|
528
|
+
...articleTable
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ❌ 避免:添加接口不需要预定义字段
|
|
532
|
+
fields: {
|
|
533
|
+
page: '@page', // 添加操作不需要分页
|
|
534
|
+
...articleTable
|
|
535
|
+
}
|
|
536
|
+
```
|
|
537
|
+
|
|
350
538
|
### 字段定义格式
|
|
351
539
|
|
|
352
540
|
```typescript
|
|
@@ -838,6 +1026,52 @@ interface BeflyContext {
|
|
|
838
1026
|
}
|
|
839
1027
|
```
|
|
840
1028
|
|
|
1029
|
+
### 常用工具方法
|
|
1030
|
+
|
|
1031
|
+
#### befly.db.cleanFields - 清理数据字段
|
|
1032
|
+
|
|
1033
|
+
清理对象中的 `null` 和 `undefined` 值,适用于处理可选参数:
|
|
1034
|
+
|
|
1035
|
+
```typescript
|
|
1036
|
+
// 方法签名
|
|
1037
|
+
befly.db.cleanFields<T>(
|
|
1038
|
+
data: T, // 要清理的数据对象
|
|
1039
|
+
excludeValues?: any[], // 要排除的值,默认 [null, undefined]
|
|
1040
|
+
keepValues?: Record<string, any> // 强制保留的键值对
|
|
1041
|
+
): Partial<T>
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
**基本用法:**
|
|
1045
|
+
|
|
1046
|
+
```typescript
|
|
1047
|
+
// 默认排除 null 和 undefined
|
|
1048
|
+
const cleanData = befly.db.cleanFields({
|
|
1049
|
+
name: 'John',
|
|
1050
|
+
age: null,
|
|
1051
|
+
email: undefined,
|
|
1052
|
+
phone: ''
|
|
1053
|
+
});
|
|
1054
|
+
// 结果: { name: 'John', phone: '' }
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
**自定义排除值:**
|
|
1058
|
+
|
|
1059
|
+
```typescript
|
|
1060
|
+
// 同时排除 null、undefined 和空字符串
|
|
1061
|
+
const cleanData = befly.db.cleanFields({ name: 'John', phone: '', age: null }, [null, undefined, '']);
|
|
1062
|
+
// 结果: { name: 'John' }
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
**保留特定字段的特定值:**
|
|
1066
|
+
|
|
1067
|
+
```typescript
|
|
1068
|
+
// 即使值在排除列表中,也保留 status 字段的 null 值
|
|
1069
|
+
const cleanData = befly.db.cleanFields({ name: 'John', status: null, count: 0 }, [null, undefined], { status: null });
|
|
1070
|
+
// 结果: { name: 'John', status: null, count: 0 }
|
|
1071
|
+
```
|
|
1072
|
+
|
|
1073
|
+
> **注意**:`insData`、`updData` 和 `where` 条件会自动调用 `cleanFields`,通常无需手动调用。
|
|
1074
|
+
|
|
841
1075
|
---
|
|
842
1076
|
|
|
843
1077
|
## 最佳实践
|
|
@@ -846,8 +1080,10 @@ interface BeflyContext {
|
|
|
846
1080
|
|
|
847
1081
|
```typescript
|
|
848
1082
|
fields: {
|
|
849
|
-
// 1.
|
|
850
|
-
|
|
1083
|
+
// 1. 首选:使用预定义字段(@id, @page, @limit, @keyword, @state)
|
|
1084
|
+
page: '@page',
|
|
1085
|
+
limit: '@limit',
|
|
1086
|
+
keyword: '@keyword',
|
|
851
1087
|
|
|
852
1088
|
// 2. 次选:引用表字段
|
|
853
1089
|
email: adminTable.email,
|
package/docs/database.md
CHANGED
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
- [核心概念](#核心概念)
|
|
10
10
|
- [DbHelper](#dbhelper)
|
|
11
11
|
- [自动转换](#自动转换)
|
|
12
|
+
- [自动过滤 null 和 undefined](#自动过滤-null-和-undefined)
|
|
13
|
+
- [写入时自动过滤](#写入时自动过滤)
|
|
14
|
+
- [更新时自动过滤](#更新时自动过滤)
|
|
15
|
+
- [Where 条件自动过滤](#where-条件自动过滤)
|
|
16
|
+
- [实际应用示例](#实际应用示例)
|
|
17
|
+
- [手动清理字段](#手动清理字段)
|
|
12
18
|
- [字段命名规范](#字段命名规范)
|
|
13
19
|
- [查询方法](#查询方法)
|
|
14
20
|
- [getOne - 查询单条](#getone---查询单条)
|
|
@@ -79,6 +85,126 @@ handler: async (befly, ctx) => {
|
|
|
79
85
|
- **字段名**:写入时小驼峰转下划线,查询时下划线转小驼峰
|
|
80
86
|
- **BIGINT 字段**:`id`、`*Id`、`*_id`、`*At`、`*_at` 自动转为 number
|
|
81
87
|
|
|
88
|
+
### 自动过滤 null 和 undefined
|
|
89
|
+
|
|
90
|
+
所有写入方法(`insData`、`insBatch`、`updData`)和条件查询(`where`)都会**自动过滤值为 `null` 或 `undefined` 的字段**。
|
|
91
|
+
|
|
92
|
+
这意味着:
|
|
93
|
+
|
|
94
|
+
- 传入 `undefined` 或 `null` 的字段会被忽略,不会写入数据库
|
|
95
|
+
- `where` 条件中值为 `undefined` 或 `null` 的条件会被忽略
|
|
96
|
+
- 这使得处理可选参数变得非常简单,无需手动判断
|
|
97
|
+
|
|
98
|
+
#### 写入时自动过滤
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// 用户提交的数据可能部分字段为空
|
|
102
|
+
await befly.db.insData({
|
|
103
|
+
table: 'user',
|
|
104
|
+
data: {
|
|
105
|
+
username: 'john',
|
|
106
|
+
email: 'john@example.com',
|
|
107
|
+
phone: undefined, // ❌ 自动忽略,不会写入
|
|
108
|
+
avatar: null, // ❌ 自动忽略,不会写入
|
|
109
|
+
nickname: '' // ✅ 空字符串会写入(不是 null/undefined)
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
// 实际 SQL: INSERT INTO user (username, email, nickname, ...) VALUES ('john', 'john@example.com', '', ...)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### 更新时自动过滤
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// 只更新用户提交的字段
|
|
119
|
+
await befly.db.updData({
|
|
120
|
+
table: 'user',
|
|
121
|
+
data: {
|
|
122
|
+
nickname: ctx.body.nickname, // 如果用户传了值,会更新
|
|
123
|
+
avatar: ctx.body.avatar, // 如果为 undefined,自动忽略
|
|
124
|
+
bio: ctx.body.bio // 如果为 null,自动忽略
|
|
125
|
+
},
|
|
126
|
+
where: { id: ctx.user.id }
|
|
127
|
+
});
|
|
128
|
+
// 只有非 null/undefined 的字段会被更新
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
#### Where 条件自动过滤
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// 条件筛选:用户可能只传部分筛选条件
|
|
135
|
+
const result = await befly.db.getList({
|
|
136
|
+
table: 'article',
|
|
137
|
+
where: {
|
|
138
|
+
categoryId: ctx.body.categoryId, // 如果未传,值为 undefined,自动忽略
|
|
139
|
+
status: ctx.body.status, // 如果未传,值为 undefined,自动忽略
|
|
140
|
+
authorId: ctx.body.authorId // 如果未传,值为 undefined,自动忽略
|
|
141
|
+
},
|
|
142
|
+
page: 1,
|
|
143
|
+
limit: 10
|
|
144
|
+
});
|
|
145
|
+
// 只有非 null/undefined 的条件会参与 WHERE 构建
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
#### 实际应用示例
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// API: 用户列表(带可选筛选条件)
|
|
152
|
+
export default {
|
|
153
|
+
name: '用户列表',
|
|
154
|
+
fields: {
|
|
155
|
+
keyword: { name: '关键词', type: 'string', max: 50 },
|
|
156
|
+
roleId: { name: '角色ID', type: 'number' },
|
|
157
|
+
state: { name: '状态', type: 'number' }
|
|
158
|
+
},
|
|
159
|
+
handler: async (befly, ctx) => {
|
|
160
|
+
// 直接使用请求参数,无需判断是否存在
|
|
161
|
+
// null/undefined 的条件会被自动过滤
|
|
162
|
+
const result = await befly.db.getList({
|
|
163
|
+
table: 'user',
|
|
164
|
+
where: {
|
|
165
|
+
roleId: ctx.body.roleId, // 未传时为 undefined,自动忽略
|
|
166
|
+
state: ctx.body.state, // 未传时为 undefined,自动忽略
|
|
167
|
+
username$like: ctx.body.keyword ? `%${ctx.body.keyword}%` : undefined // 无关键词时忽略
|
|
168
|
+
},
|
|
169
|
+
page: ctx.body.page || 1,
|
|
170
|
+
limit: ctx.body.limit || 10
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return befly.tool.Yes('查询成功', result);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
#### 手动清理字段
|
|
179
|
+
|
|
180
|
+
如需手动清理数据,可以使用 `cleanFields` 方法:
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
// 默认排除 null 和 undefined
|
|
184
|
+
const cleanData = befly.db.cleanFields({
|
|
185
|
+
name: 'John',
|
|
186
|
+
age: null,
|
|
187
|
+
email: undefined,
|
|
188
|
+
phone: ''
|
|
189
|
+
});
|
|
190
|
+
// 结果: { name: 'John', phone: '' }
|
|
191
|
+
|
|
192
|
+
// 自定义排除值(如同时排除空字符串)
|
|
193
|
+
const cleanData2 = befly.db.cleanFields(
|
|
194
|
+
{ name: 'John', phone: '', age: null },
|
|
195
|
+
[null, undefined, ''] // 排除这些值
|
|
196
|
+
);
|
|
197
|
+
// 结果: { name: 'John' }
|
|
198
|
+
|
|
199
|
+
// 保留特定字段的特定值(即使在排除列表中)
|
|
200
|
+
const cleanData3 = befly.db.cleanFields(
|
|
201
|
+
{ name: 'John', status: null, count: 0 },
|
|
202
|
+
[null, undefined], // 排除 null 和 undefined
|
|
203
|
+
{ status: null } // 但保留 status 字段的 null 值
|
|
204
|
+
);
|
|
205
|
+
// 结果: { name: 'John', status: null, count: 0 }
|
|
206
|
+
```
|
|
207
|
+
|
|
82
208
|
---
|
|
83
209
|
|
|
84
210
|
## 字段命名规范
|
|
@@ -889,14 +1015,32 @@ orderBy: ['sort#ASC', 'id#DESC'];
|
|
|
889
1015
|
|
|
890
1016
|
### 默认 State 过滤
|
|
891
1017
|
|
|
892
|
-
所有查询方法默认添加 `state > 0`
|
|
1018
|
+
所有查询方法默认添加 `state > 0` 条件,**仅过滤软删除的数据(state=0)**。
|
|
1019
|
+
|
|
1020
|
+
**过滤效果**:
|
|
1021
|
+
|
|
1022
|
+
| state 值 | 默认查询结果 |
|
|
1023
|
+
| -------- | ------------------- |
|
|
1024
|
+
| 0 | ❌ 被过滤(软删除) |
|
|
1025
|
+
| 1 | ✅ 可查询(正常) |
|
|
1026
|
+
| 2 | ✅ 可查询(禁用) |
|
|
1027
|
+
|
|
1028
|
+
> ⚠️ **注意**:禁用数据(state=2)默认**可以**查询到,如需过滤禁用数据,需显式指定 `state: 1`。
|
|
893
1029
|
|
|
894
1030
|
```typescript
|
|
895
|
-
//
|
|
1031
|
+
// 默认查询:state > 0,包含正常和禁用数据
|
|
896
1032
|
getOne({ table: 'user', where: { id: 1 } });
|
|
897
1033
|
// → WHERE id = 1 AND state > 0
|
|
898
1034
|
|
|
899
|
-
//
|
|
1035
|
+
// 只查询正常状态的数据
|
|
1036
|
+
getList({ table: 'user', where: { state: 1 } });
|
|
1037
|
+
// → WHERE state = 1
|
|
1038
|
+
|
|
1039
|
+
// 只查询禁用状态的数据
|
|
1040
|
+
getList({ table: 'user', where: { state: 2 } });
|
|
1041
|
+
// → WHERE state = 2
|
|
1042
|
+
|
|
1043
|
+
// 查询所有状态(包括软删除)
|
|
900
1044
|
getOne({ table: 'user', where: { id: 1, state$gte: 0 } });
|
|
901
1045
|
// → WHERE id = 1 AND state >= 0
|
|
902
1046
|
```
|
package/docs/examples.md
CHANGED
|
@@ -284,10 +284,10 @@ export default {
|
|
|
284
284
|
auth: true,
|
|
285
285
|
permission: 'user:list',
|
|
286
286
|
fields: {
|
|
287
|
-
page:
|
|
288
|
-
limit:
|
|
289
|
-
keyword:
|
|
290
|
-
state:
|
|
287
|
+
page: '@page',
|
|
288
|
+
limit: '@limit',
|
|
289
|
+
keyword: '@keyword',
|
|
290
|
+
state: '@state',
|
|
291
291
|
role: { name: '角色', type: 'string', max: 20 }
|
|
292
292
|
},
|
|
293
293
|
handler: async (befly, ctx) => {
|
|
@@ -429,7 +429,7 @@ export default {
|
|
|
429
429
|
method: 'POST',
|
|
430
430
|
auth: true,
|
|
431
431
|
fields: {
|
|
432
|
-
id:
|
|
432
|
+
id: '@id',
|
|
433
433
|
title: { name: '标题', type: 'string', min: 2, max: 200 },
|
|
434
434
|
content: { name: '内容', type: 'text', min: 1, max: 100000 },
|
|
435
435
|
summary: { name: '摘要', type: 'string', max: 500 },
|
|
@@ -510,7 +510,7 @@ export default {
|
|
|
510
510
|
method: 'POST',
|
|
511
511
|
auth: true,
|
|
512
512
|
fields: {
|
|
513
|
-
id:
|
|
513
|
+
id: '@id'
|
|
514
514
|
},
|
|
515
515
|
required: ['id'],
|
|
516
516
|
handler: async (befly, ctx) => {
|
|
@@ -561,11 +561,11 @@ export default {
|
|
|
561
561
|
method: 'POST',
|
|
562
562
|
auth: false,
|
|
563
563
|
fields: {
|
|
564
|
-
page:
|
|
565
|
-
limit:
|
|
564
|
+
page: '@page',
|
|
565
|
+
limit: '@limit',
|
|
566
|
+
keyword: '@keyword',
|
|
566
567
|
categoryId: { name: '分类', type: 'number', min: 0 },
|
|
567
568
|
authorId: { name: '作者', type: 'number', min: 0 },
|
|
568
|
-
keyword: { name: '关键词', type: 'string', max: 50 },
|
|
569
569
|
isTop: { name: '置顶', type: 'number', min: 0, max: 1 },
|
|
570
570
|
isRecommend: { name: '推荐', type: 'number', min: 0, max: 1 },
|
|
571
571
|
orderBy: { name: '排序', type: 'string', max: 20 }
|
|
@@ -615,7 +615,7 @@ export default {
|
|
|
615
615
|
method: 'GET',
|
|
616
616
|
auth: false,
|
|
617
617
|
fields: {
|
|
618
|
-
id:
|
|
618
|
+
id: '@id'
|
|
619
619
|
},
|
|
620
620
|
required: ['id'],
|
|
621
621
|
handler: async (befly, ctx) => {
|
|
@@ -771,6 +771,99 @@ export default {
|
|
|
771
771
|
|
|
772
772
|
---
|
|
773
773
|
|
|
774
|
+
## 代码优化技巧
|
|
775
|
+
|
|
776
|
+
### 利用自动过滤简化更新操作
|
|
777
|
+
|
|
778
|
+
数据库操作会**自动过滤 null 和 undefined 值**,因此可以大幅简化代码。
|
|
779
|
+
|
|
780
|
+
#### 传统写法(繁琐)
|
|
781
|
+
|
|
782
|
+
```typescript
|
|
783
|
+
// ❌ 手动检查每个字段
|
|
784
|
+
const updateData: Record<string, any> = {};
|
|
785
|
+
if (ctx.body.nickname !== undefined) updateData.nickname = ctx.body.nickname;
|
|
786
|
+
if (ctx.body.avatar !== undefined) updateData.avatar = ctx.body.avatar;
|
|
787
|
+
if (ctx.body.phone !== undefined) updateData.phone = ctx.body.phone;
|
|
788
|
+
if (ctx.body.gender !== undefined) updateData.gender = ctx.body.gender;
|
|
789
|
+
|
|
790
|
+
if (Object.keys(updateData).length === 0) {
|
|
791
|
+
return No('没有需要更新的字段');
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
await befly.db.updData({
|
|
795
|
+
table: 'user',
|
|
796
|
+
data: updateData,
|
|
797
|
+
where: { id: ctx.user.userId }
|
|
798
|
+
});
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
#### 优化写法(简洁)
|
|
802
|
+
|
|
803
|
+
```typescript
|
|
804
|
+
// ✅ 直接传入,undefined 值自动过滤
|
|
805
|
+
const { nickname, avatar, phone, gender, birthday, bio } = ctx.body;
|
|
806
|
+
|
|
807
|
+
const data = { nickname: nickname, avatar: avatar, phone: phone, gender: gender, birthday: birthday, bio: bio };
|
|
808
|
+
|
|
809
|
+
// 使用 cleanFields 检查是否有有效数据
|
|
810
|
+
const cleanData = befly.tool.cleanFields(data);
|
|
811
|
+
if (Object.keys(cleanData).length === 0) {
|
|
812
|
+
return No('没有需要更新的字段');
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
await befly.db.updData({
|
|
816
|
+
table: 'user',
|
|
817
|
+
data: cleanData,
|
|
818
|
+
where: { id: ctx.user.userId }
|
|
819
|
+
});
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
### 使用 cleanFields 进行精细控制
|
|
823
|
+
|
|
824
|
+
当需要保留特定值(如 0、空字符串)时,使用 `cleanFields` 的高级参数:
|
|
825
|
+
|
|
826
|
+
```typescript
|
|
827
|
+
const { nickname, sort, state, remark } = ctx.body;
|
|
828
|
+
|
|
829
|
+
// 保留 0 值(sort 和 state 允许为 0)
|
|
830
|
+
const data = befly.tool.cleanFields(
|
|
831
|
+
{ nickname: nickname, sort: sort, state: state, remark: remark },
|
|
832
|
+
[null, undefined], // 排除 null 和 undefined
|
|
833
|
+
{ sort: true, state: true } // 保留这些字段的 0 值
|
|
834
|
+
);
|
|
835
|
+
|
|
836
|
+
await befly.db.updData({
|
|
837
|
+
table: 'menu',
|
|
838
|
+
data: data,
|
|
839
|
+
where: { id: ctx.body.id }
|
|
840
|
+
});
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
### 查询条件的自动过滤
|
|
844
|
+
|
|
845
|
+
where 条件同样支持自动过滤:
|
|
846
|
+
|
|
847
|
+
```typescript
|
|
848
|
+
// ✅ 可选筛选条件,undefined 自动忽略
|
|
849
|
+
const { keyword, state, categoryId, startDate, endDate } = ctx.body;
|
|
850
|
+
|
|
851
|
+
const result = await befly.db.getList({
|
|
852
|
+
table: 'article',
|
|
853
|
+
columns: ['id', 'title', 'createdAt'],
|
|
854
|
+
where: {
|
|
855
|
+
state: state, // undefined 时忽略
|
|
856
|
+
categoryId: categoryId, // undefined 时忽略
|
|
857
|
+
title: keyword ? { $like: `%${keyword}%` } : undefined, // 无关键词时忽略
|
|
858
|
+
createdAt: startDate && endDate ? { $gte: startDate, $lte: endDate } : undefined
|
|
859
|
+
},
|
|
860
|
+
page: ctx.body.page || 1,
|
|
861
|
+
limit: ctx.body.limit || 20
|
|
862
|
+
});
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
---
|
|
866
|
+
|
|
774
867
|
## 完整目录结构
|
|
775
868
|
|
|
776
869
|
```
|
package/docs/validator.md
CHANGED
|
@@ -586,3 +586,33 @@ A: 数组类型会验证:
|
|
|
586
586
|
1. 值是否为数组
|
|
587
587
|
2. 元素数量是否在 min/max 范围内
|
|
588
588
|
3. 如果有 regexp,每个元素都会进行正则验证
|
|
589
|
+
|
|
590
|
+
### Q: 如何在验证前清理数据中的 null/undefined 值?
|
|
591
|
+
|
|
592
|
+
A: 使用 `befly.tool.cleanFields` 方法:
|
|
593
|
+
|
|
594
|
+
```typescript
|
|
595
|
+
// 在 API handler 中使用
|
|
596
|
+
handler: async (befly, ctx) => {
|
|
597
|
+
const { nickname, phone, address } = ctx.body;
|
|
598
|
+
|
|
599
|
+
// 清理 null 和 undefined 值
|
|
600
|
+
const cleanData = befly.tool.cleanFields({
|
|
601
|
+
nickname: nickname,
|
|
602
|
+
phone: phone,
|
|
603
|
+
address: address
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
// cleanData 只包含有效值
|
|
607
|
+
await befly.db.updData({
|
|
608
|
+
table: 'user',
|
|
609
|
+
data: cleanData,
|
|
610
|
+
where: { id: ctx.user.userId }
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
return Yes('更新成功');
|
|
614
|
+
};
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
> **注意**:数据库操作(insData、updData 等)会自动过滤 null/undefined 值,通常不需要手动调用 cleanFields。
|
|
618
|
+
> 详见 [database.md](./database.md#nullundefined-值自动过滤)。
|
package/loader/loadApis.ts
CHANGED
|
@@ -20,42 +20,39 @@ import { projectApiDir } from '../paths.js';
|
|
|
20
20
|
import type { ApiRoute } from '../types/api.js';
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
* 这些字段会自动合并到所有 API 的 fields 中
|
|
25
|
-
* API 自定义的同名字段可以覆盖这些默认值
|
|
23
|
+
* 预定义的默认字段
|
|
26
24
|
*/
|
|
27
|
-
const
|
|
28
|
-
id: {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
min: 0,
|
|
56
|
-
max: 2
|
|
25
|
+
const PRESET_FIELDS: Record<string, any> = {
|
|
26
|
+
'@id': { name: 'ID', type: 'number', min: 1, max: null },
|
|
27
|
+
'@page': { name: '页码', type: 'number', min: 1, max: 9999 },
|
|
28
|
+
'@limit': { name: '每页数量', type: 'number', min: 1, max: 100 },
|
|
29
|
+
'@keyword': { name: '关键词', type: 'string', min: 1, max: 50 },
|
|
30
|
+
'@state': { name: '状态', type: 'number', min: 0, max: 2 }
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 处理字段定义,将 @ 符号引用替换为实际字段定义
|
|
35
|
+
*/
|
|
36
|
+
function processFields(fields: Record<string, any>): Record<string, any> {
|
|
37
|
+
if (!fields || typeof fields !== 'object') return fields;
|
|
38
|
+
|
|
39
|
+
const processed: Record<string, any> = {};
|
|
40
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
41
|
+
// 如果值是字符串且以 @ 开头,则查找预定义字段
|
|
42
|
+
if (typeof value === 'string' && value.startsWith('@')) {
|
|
43
|
+
if (PRESET_FIELDS[value]) {
|
|
44
|
+
processed[key] = PRESET_FIELDS[value];
|
|
45
|
+
} else {
|
|
46
|
+
// 未找到预定义字段,保持原值
|
|
47
|
+
processed[key] = value;
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
// 普通字段定义,保持原样
|
|
51
|
+
processed[key] = value;
|
|
52
|
+
}
|
|
57
53
|
}
|
|
58
|
-
|
|
54
|
+
return processed;
|
|
55
|
+
}
|
|
59
56
|
|
|
60
57
|
/**
|
|
61
58
|
* 加载所有 API 路由
|
|
@@ -113,8 +110,8 @@ export async function loadApis(apis: Map<string, ApiRoute>): Promise<void> {
|
|
|
113
110
|
// 设置默认值
|
|
114
111
|
const methodStr = (api.method || 'POST').toUpperCase();
|
|
115
112
|
api.auth = api.auth !== undefined ? api.auth : true;
|
|
116
|
-
//
|
|
117
|
-
api.fields =
|
|
113
|
+
// 处理字段定义,将 @ 引用替换为实际字段定义
|
|
114
|
+
api.fields = processFields(api.fields || {});
|
|
118
115
|
api.required = api.required || [];
|
|
119
116
|
|
|
120
117
|
// 构建路由路径(不含方法)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "befly",
|
|
3
|
-
"version": "3.9.
|
|
3
|
+
"version": "3.9.21",
|
|
4
4
|
"description": "Befly - 为 Bun 专属打造的 TypeScript API 接口框架核心引擎",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"pino": "^10.1.0",
|
|
75
75
|
"pino-roll": "^4.0.0"
|
|
76
76
|
},
|
|
77
|
-
"gitHead": "
|
|
77
|
+
"gitHead": "d0825d638bbb779137fb8860049bcc1b3200a5dc",
|
|
78
78
|
"devDependencies": {
|
|
79
79
|
"typescript": "^5.9.3"
|
|
80
80
|
}
|
package/sync/syncDb/table.ts
CHANGED
|
@@ -161,7 +161,8 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
|
|
|
161
161
|
const dbFieldName = snakeCase(fieldKey);
|
|
162
162
|
|
|
163
163
|
const indexName = `idx_${dbFieldName}`;
|
|
164
|
-
|
|
164
|
+
// 如果字段有 unique 约束,跳过创建普通索引(unique 会自动创建唯一索引)
|
|
165
|
+
if (fieldDef.index && !fieldDef.unique && !existingIndexes[indexName]) {
|
|
165
166
|
indexActions.push({ action: 'create', indexName: indexName, fieldName: dbFieldName });
|
|
166
167
|
changed = true;
|
|
167
168
|
} else if (!fieldDef.index && existingIndexes[indexName] && existingIndexes[indexName].length === 1) {
|