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.
@@ -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
- 所有 API 自动包含以下默认字段,无需重复定义:
313
+ 框架提供了一套预定义字段系统,通过 `@` 符号引用常用字段,避免重复定义。
314
+
315
+ #### 可用预定义字段
314
316
 
315
317
  ```typescript
316
- const DEFAULT_API_FIELDS = {
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. 首选:使用默认字段(id, page, limit, keyword, state)
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
- // 如需查询所有状态,显式指定 state 条件
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: { name: '页码', type: 'number', min: 1, default: 1 },
288
- limit: { name: '每页数量', type: 'number', min: 1, max: 100, default: 20 },
289
- keyword: { name: '关键词', type: 'string', max: 50 },
290
- state: { name: '状态', type: 'number', min: -1, max: 1 },
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: { name: '文章ID', type: 'number', min: 1 },
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: { name: '文章ID', type: 'number', min: 1 }
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: { name: '页码', type: 'number', min: 1, default: 1 },
565
- limit: { name: '每页数量', type: 'number', min: 1, max: 50, default: 10 },
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: { name: '文章ID', type: 'number', min: 1 }
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-值自动过滤)。
@@ -20,42 +20,39 @@ import { projectApiDir } from '../paths.js';
20
20
  import type { ApiRoute } from '../types/api.js';
21
21
 
22
22
  /**
23
- * API 默认字段定义
24
- * 这些字段会自动合并到所有 API 的 fields 中
25
- * API 自定义的同名字段可以覆盖这些默认值
23
+ * 预定义的默认字段
26
24
  */
27
- const DEFAULT_API_FIELDS = {
28
- id: {
29
- name: 'ID',
30
- type: 'number',
31
- min: 1,
32
- max: null
33
- },
34
- page: {
35
- name: '页码',
36
- type: 'number',
37
- min: 1,
38
- max: 9999
39
- },
40
- limit: {
41
- name: '每页数量',
42
- type: 'number',
43
- min: 1,
44
- max: 100
45
- },
46
- keyword: {
47
- name: '关键词',
48
- type: 'string',
49
- min: 1,
50
- max: 50
51
- },
52
- state: {
53
- name: '状态',
54
- type: 'number',
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
- } as const;
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
- // 合并默认字段:默认字段作为基础,API 自定义字段优先级更高
117
- api.fields = { ...DEFAULT_API_FIELDS, ...(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.13",
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": "f0dc38476a87163a9de1d5ddcefa42304b1ee09b",
77
+ "gitHead": "d0825d638bbb779137fb8860049bcc1b3200a5dc",
78
78
  "devDependencies": {
79
79
  "typescript": "^5.9.3"
80
80
  }
@@ -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
- if (fieldDef.index && !existingIndexes[indexName]) {
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) {