wok-server 0.7.1 → 0.8.0

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.
Files changed (42) hide show
  1. package/README.md +101 -14
  2. package/dist/http-client/index.js +5 -4
  3. package/dist/lock/index.js +5 -2
  4. package/dist/mvc/config.js +4 -2
  5. package/dist/mvc/exchange.js +32 -7
  6. package/dist/mvc/index.js +1 -1
  7. package/dist/mvc/server.js +12 -4
  8. package/dist/mysql/manager/base.js +5 -8
  9. package/dist/mysql/manager/ops/delete.js +2 -1
  10. package/dist/mysql/manager/ops/find.js +8 -4
  11. package/dist/mysql/manager/ops/insert.js +15 -40
  12. package/dist/mysql/manager/ops/update.js +2 -1
  13. package/dist/mysql/manager/ops/upsert.js +11 -31
  14. package/dist/mysql/migration.js +10 -3
  15. package/documentation/en/mysql.md +26 -33
  16. package/documentation/zh-cn/mysql.md +27 -34
  17. package/documentation/zh-cn/philosophy.md +433 -0
  18. package/package.json +1 -1
  19. package/skills/wok-server-api-rules/SKILL.md +113 -0
  20. package/skills/wok-server-code-navigation/SKILL.md +169 -95
  21. package/skills/wok-server-mysql/SKILL.md +16 -10
  22. package/skills/wok-server-mysql/references/version-control.md +7 -5
  23. package/src/http-client/index.ts +8 -7
  24. package/src/lock/index.ts +5 -2
  25. package/src/mvc/config.ts +9 -2
  26. package/src/mvc/exchange.ts +31 -6
  27. package/src/mvc/index.ts +1 -1
  28. package/src/mvc/server.ts +11 -5
  29. package/src/mysql/manager/base.ts +17 -17
  30. package/src/mysql/manager/ops/delete.ts +2 -1
  31. package/src/mysql/manager/ops/find.ts +8 -4
  32. package/src/mysql/manager/ops/insert.ts +23 -61
  33. package/src/mysql/manager/ops/update.ts +2 -1
  34. package/src/mysql/manager/ops/upsert.ts +31 -51
  35. package/src/mysql/migration.ts +14 -3
  36. package/types/http-client/index.d.ts +4 -4
  37. package/types/lock/index.d.ts +3 -0
  38. package/types/mvc/config.d.ts +5 -0
  39. package/types/mvc/exchange.d.ts +13 -3
  40. package/types/mysql/manager/base.d.ts +12 -12
  41. package/types/mysql/manager/ops/insert.d.ts +2 -16
  42. package/types/mysql/manager/ops/upsert.d.ts +3 -4
@@ -242,6 +242,119 @@ await startWebServer({
242
242
 
243
243
  ---
244
244
 
245
+ ## 规则 5:MySQL 更新方法传参——利用框架自动忽略 `undefined`
246
+
247
+ wok-server MySQL 组件会自动忽略值为 `undefined` 的字段,**不要**手动构建 `updateData` 对象:
248
+
249
+ ```ts
250
+ // ❌ 啰嗦,不推荐
251
+ const updateData: Partial<Knowledge> = {}
252
+ if (body.name !== undefined) updateData.name = body.name
253
+ if (body.desc !== undefined) updateData.desc = body.desc
254
+
255
+ await session.updateOne(tableKnowledge, where, updateData)
256
+
257
+ // ✅ 简洁直观,推荐
258
+ await session.updateOne(tableKnowledge, where, {
259
+ name: body.name,
260
+ desc: body.desc,
261
+ question_prompt: body.question_prompt
262
+ })
263
+ ```
264
+
265
+ ---
266
+
267
+ ## 规则 6:MySQL 更新接口参数必填性——与实体类保持一致
268
+
269
+ 更新接口中出现的字段,其必填性应与实体类一致。实体中必填的字段,在更新接口中不应设为可选:
270
+
271
+ ```ts
272
+ // ❌ 不推荐:name 在实体中是必填,这里却设成了可选
273
+ interface UpdateForm {
274
+ id: string
275
+ name?: string
276
+ desc?: string
277
+ }
278
+
279
+ // ✅ 推荐:出现的字段与实体类保持一致的必填性
280
+ interface UpdateForm {
281
+ id: string
282
+ name: string
283
+ desc?: string
284
+ }
285
+ ```
286
+
287
+ ---
288
+
289
+ ## 规则 7:Entity 状态字段约束——禁止 `string`,必须使用联合类型
290
+
291
+ Entity 中的**状态字段禁止使用 `string` 类型**,必须使用**联合类型**精确约束所有合法值:
292
+
293
+ ```ts
294
+ // ❌ 错误:使用 string 类型,没有任何约束
295
+ interface ChatbotSession {
296
+ status: string // 任何字符串都可以通过编译
297
+ }
298
+
299
+ // ✅ 正确:使用联合类型,精确约束
300
+ interface ChatbotSession {
301
+ /**
302
+ * 会话状态
303
+ */
304
+ status: '临时' | '活跃'
305
+ }
306
+
307
+ interface Chatbot {
308
+ /**
309
+ * 状态
310
+ */
311
+ status: '未发布' | '已发布'
312
+ }
313
+ ```
314
+
315
+ **为什么必须这样做**:联合类型提供编译期检查,`string` 类型允许任何值,一旦写错(如 `'temprary'` vs `'临时'`)不会报错,运行时难以排查。
316
+
317
+ **检查方法**:定义或修改 Entity 时,检查每个 `status` 字段,确保它是联合类型而非 `string`。
318
+
319
+ ---
320
+
321
+ ## 规则 8:API Response 接口中枚举/联合类型必须引用实体类型
322
+
323
+ 在 API 的 Response 接口中定义字段时:
324
+ - **枚举/联合类型字段**(如 `status`、`type`)**必须引用实体的类型定义**,禁止手写字面量值或使用 `number`/`string`
325
+ - **原始类型字段**(如 `number`、`string`、`boolean`)直接用原始类型,不要过度引用
326
+
327
+ ```ts
328
+ // ❌ 错误:手写字面量值,与实体定义脱节
329
+ interface Resp {
330
+ status?: number // number 无法约束合法值
331
+ learning_task?: {
332
+ type: '视频' | '文档' | '考试' // 实体变了这里不会同步
333
+ }
334
+ }
335
+
336
+ // ❌ 错误:原始类型也去引用实体,过度设计
337
+ interface Resp {
338
+ duration: LearningTask['duration'] // duration 本来就是 number,没必要
339
+ }
340
+
341
+ // ✅ 正确:枚举/联合类型引用实体,原始类型直接用
342
+ import { User } from '../../user'
343
+ import { LearningTask } from '../learning_task/learning-task'
344
+
345
+ interface Resp {
346
+ status?: User['status'] // 引用实体的联合类型
347
+ learning_task?: {
348
+ type: LearningTask['type'] // 引用实体的联合类型
349
+ duration: number // 原始类型直接用
350
+ }
351
+ }
352
+ ```
353
+
354
+ **为什么**:联合类型的手写字面量会与实体定义脱节。当实体新增/修改合法值时,Response 接口不会被同步更新,导致类型泄露。引用实体类型确保两者始终一致。
355
+
356
+ ---
357
+
245
358
  ## 附录 A:ServerExchange 速查表
246
359
 
247
360
  所有路由 handler 和拦截器都通过 `ServerExchange` 与请求/响应交互。以下是其完整方法列表,禁止使用表中未列出的方法。
@@ -11,143 +11,217 @@ metadata:
11
11
 
12
12
  ## 概述
13
13
 
14
- 面对一个 wok-server 项目时,按以下路径快速理解项目结构、定位目标代码。核心原则:**项目按功能划分目录,路由集中配置,每个接口/拦截器独立一个文件**。
14
+ 面对一个基于 wok-server 的项目时,按以下路径快速理解项目结构、定位目标代码。核心原则:**项目按功能划分目录,路由集中配置,每个接口/拦截器独立一个文件**。
15
15
 
16
- ## 项目入口 — main.ts
16
+ ## 通用
17
17
 
18
- 入口文件在 `src/main.ts`,包含:
18
+ ### 找到后端代码位置
19
19
 
20
- 1. 全局初始化(如 `Date.prototype.toJSON`、`enableMysql()`)
21
- 2. `startWebServer()` 调用,配置 routers 和 interceptors
22
- 3. 任务调度(`scheduleWithFixedDelay` 等)
20
+ 一个项目可能前后端分离,也可能前端项目合并在同一个仓库。按以下顺序排查:
23
21
 
24
- **所以 `main.ts` 是读懂项目的第一个文件**。从这里可以看到所有路由映射和拦截器链。
22
+ #### 1. 检查根目录
25
23
 
26
- ## 路由定位决策树
24
+ 查看项目根目录是否有 `package.json`:
27
25
 
26
+ ```bash
27
+ ls package.json
28
28
  ```
29
- 要找一个接口的处理逻辑?
30
- 打开 src/main.ts(或 src/router.ts)
31
- → 找到路由对象中对应路径的 key
32
- → 看它的 value(通常是 import 的 handler 函数)
33
- Ctrl/Cmd + 点击跳转到 handler 文件
29
+
30
+ 如果有,搜索其中是否依赖了 `wok-server`:
31
+
32
+ ```bash
33
+ grep -o '"wok-server"' package.json
34
34
  ```
35
35
 
36
- 路由集中在一个地方,所以**搜路由路径就能找到对应 handler 的 import**。
36
+ 如果匹配,说明根目录就是后端项目,后端代码就在根目录。
37
+
38
+ #### 2. 检查 backend / server 目录
37
39
 
38
- ## 文件定位速查表
40
+ 如果根目录没有 `package.json` 或没有安装 wok-server,查找 `backend/` 或 `server/` 子目录:
39
41
 
40
- | 想找什么 | 去哪里找 |
41
- | ------------------------ | -------------------------------------------------------------- |
42
- | 入口、路由注册、启动逻辑 | `src/main.ts` |
43
- | 路由配置(大项目) | `src/router.ts` 或 `src/router/` 目录 |
44
- | 某个接口的处理逻辑 | `src/{功能名}/{接口名}.ts`(如 `src/user/create-user.ts`) |
45
- | 拦截器 | `src/{功能名}/{拦截器名}-interceptor.ts` 或 `src/exception.ts` |
46
- | 数据库实体与表定义 | `src/{功能名}/{功能名}.ts`(如 `user/user.ts`) |
47
- | MySQL 迁移脚本 | `db_migration/` 目录 |
48
- | 环境变量 | 项目根目录 `.env` |
42
+ ```bash
43
+ ls backend/package.json
44
+ ```
49
45
 
50
- ## 目录结构速览
46
+ 搜索其中是否依赖了 `wok-server`。例如一个前后端分离的项目,后端代码可能在 `backend/` 目录:
51
47
 
52
48
  ```
53
- 根目录
54
- ├── db_migration/ # MySQL 迁移文件
49
+ backend/
50
+ ├── package.json # 包含 "wok-server": "^0.5.0"
55
51
  ├── src/
56
- ├── auth/ # 授权功能
57
- │ │ ├── auth.ts # 实体配置(表结构)
58
- │ │ ├── auth-interceptor.ts # 授权拦截器
59
- │ │ ├── create-auth.ts # 创建授权接口
60
- │ │ └── index.ts # 包入口,聚合导出
61
- │ ├── user/ # 用户功能
62
- │ │ ├── user.ts
63
- │ │ ├── create-user.ts
64
- │ │ └── index.ts
65
- │ ├── exception.ts # 全局异常定义 + 异常拦截器
66
- │ ├── router.ts # 路由集中配置(大项目)
67
- │ └── main.ts # 入口文件(小项目路由也在这里)
68
- ├── .env
69
- ├── package.json
70
- └── tsconfig.json
52
+ ├── db-migration/
53
+ └── docker/
71
54
  ```
72
55
 
73
- ## 阅读项目的步骤
56
+ #### 3. 如果以上都行不通
74
57
 
75
- ### 第一步:看 main.ts
58
+ - 搜索项目中的 `wok-server` 关键词(如 `from 'wok-server'`)
59
+ - 搜索 `startWebServer` 函数调用,这是 wok-server 的入口 API
60
+ - 搜索 `createJsonHandler`、`Table<`、`Routers` 等 wok-server 特有类型
76
61
 
77
- 打开 `src/main.ts`,关注三个区域:
62
+ > **确定了后端代码目录后**,后续所有文件路径都相对于该目录(以下称为**项目根目录**)。
78
63
 
79
- 1. **顶部 import** — 了解项目用了哪些 wok-server 功能模块(MySQL?MongoDB?Task?)
80
- 2. **startWebServer() 调用** — 了解有哪些路由和拦截器
81
- 3. **任务调度调用** — 了解有哪些定时任务
64
+ ### 真实项目结构示例
82
65
 
83
- ### 第二步:看路由表
66
+ 一个典型 wok-server 中型项目的目录结构(仅作参考,实际项目可能不同):
84
67
 
85
- 从 `main.ts` 的 routers 对象中找到你关心的路径,跳转到对应 handler。
68
+ ```
69
+ backend/
70
+ ├── db-migration/ # MySQL 迁移文件
71
+ └── src/
72
+ ├── main.ts # 入口文件
73
+ ├── exception.ts # 全局异常定义 + 异常拦截器
74
+ ├── router/
75
+ │ ├── json-handler.ts # 自定义 handler 工厂(二次封装)
76
+ │ └── rules/ # 按业务模块拆分的路由规则
77
+ │ ├── index.ts # 聚合所有模块路由
78
+ │ ├── contact.ts # 联系人模块路由
79
+ │ └── order.ts # 订单模块路由
80
+ ├── contact/ # 联系人业务模块
81
+ │ ├── contact.ts # 数据实体 & 表定义
82
+ │ ├── contact-service.ts # 业务逻辑层
83
+ │ ├── bs-contact-create.ts
84
+ │ ├── bs-contact-get.ts
85
+ │ ├── field/ # 子功能:自定义字段
86
+ │ └── log/ # 子功能:操作日志
87
+ ├── order/ # 订单模块
88
+ ├── auth/ # 授权模块
89
+ ├── redis/ # Redis 基础设施(项目扩展)
90
+ └── ali-oss/ # OSS 基础设施(项目扩展)
91
+ ```
86
92
 
87
- ### 第三步:看拦截器链
93
+ ### 搜索技巧
88
94
 
89
- 从 `main.ts` 的 interceptors 数组了解请求处理流程。典型链路:
95
+ 如果以上方法定位不到文件,全文搜索以下关键词:
90
96
 
91
- ```
92
- 请求 globalErrorInterceptor authInterceptor → handler
93
- ```
97
+ 1. **路由路径字符串** — 一定能找到 handler 的 import 来源(因为 wok-server 路由使用完整路径作为 key)
98
+ 2. `createJsonHandler` —— 列出直接使用内置工厂的 JSON 接口
99
+ 3. `create.*Handler` —— 发现自定义封装的 handler 工厂及使用位置
100
+ 4. `Table<` —— 列出所有数据表定义
101
+ 5. `Interceptor` —— 列出所有拦截器
102
+ 6. `schedule` —— 列出所有定时任务
103
+ 7. `from 'wok-server'` —— 列出所有使用 wok-server API 的文件
94
104
 
95
- ### 第四步:按需看业务目录
105
+ ## MVC(路由 / 拦截器 / Handler)
96
106
 
97
- 根据 handler 文件所在的目录,查看同目录下的:
107
+ ### 项目入口 — main.ts
98
108
 
99
- - `{功能}.ts` — 了解数据实体结构
100
- - `{功能}-interceptor.ts` — 了解功能级拦截逻辑
101
- - 其他 handler 文件 — 了解该功能的完整接口
109
+ 入口文件在 `src/main.ts`,包含以下典型结构(以真实项目为例):
102
110
 
103
- ## 常见查找场景
111
+ ```ts
112
+ // 1. 全局初始化
113
+ Date.prototype.toJSON = function () { return this.getTime() as any }
114
+ process.on('uncaughtException', err => getLogger().error('未捕获的异常', err))
115
+
116
+ // 2. 启用基础设施
117
+ await enableRedis() // 项目扩展的,非框架内置
118
+ await enableMysql() // wok-server 框架自带的
119
+ await enableAliOss() // 项目扩展的,非框架内置
120
+
121
+ // 3. 启动 Web 服务 —— 配置路由和拦截器
122
+ await startWebServer({
123
+ interceptors: [globalErrorInterceptor],
124
+ routers: { '/': homepage, ...routers }
125
+ })
126
+
127
+ // 4. 定时任务
128
+ scheduleWithFixedDelay(30, 30, new MessageQueueTask({...}))
129
+ scheduleDailyTask(3, 0, new CorpFileCleanTask())
130
+ ```
131
+
132
+ **所以 `main.ts` 是读懂项目的第一个文件**。从它可以看到:
133
+ - 项目用了哪些基础设施(框架自带的如 `enableMysql()`,项目扩展的如 `enableRedis()`、`enableAliOss()`)
134
+ - **拦截器链**:`interceptors` 数组定义了请求处理流程,例如 `请求 → globalErrorInterceptor → handler`
135
+ - **路由入口**:`routers` 对象指明了路由配置的位置
136
+ - **定时任务**:`scheduleWithFixedDelay`、`scheduleDailyTask` 等
104
137
 
105
- ### 场景 1:我要改一个接口的逻辑
138
+ ### 从接口路径定位 handler(最可靠的方法)
106
139
 
107
- 1. 从路由路径反查 handler 文件(搜路径字符串)
108
- 2. 打开 handler 文件,找到 `handle()` 函数体
109
- 3. 修改逻辑
140
+ wok-server 的路由配置使用接口完整路径作为 key,所以**在 `src/` 目录下搜索接口路径字符串,就能直接定位到 handler 的 import 位置**。不需要依赖固定的文件名或目录结构。
141
+
142
+ ```
143
+ 要找一个接口的处理逻辑?
144
+ → 在 src/ 目录下搜索接口完整路径(如 '/bs/contact/create')
145
+ → 找到匹配的路由配置位置
146
+ → 在该行看到 handler 的 import 来源
147
+ → Ctrl/Cmd + 点击跳转到 handler 文件
148
+ ```
110
149
 
111
- ### 场景 2:我要新增一个接口
150
+ #### 关键原则
112
151
 
113
- 1. 在对应功能目录下新建 `xxx.ts`
114
- 2. `createJsonHandler` 或其他工厂函数创建 handler
115
- 3. `src/main.ts` 或 `src/router.ts` 中注册路由
152
+ - **最可靠的方式**:从 `startWebServer()`(在 `src/main.ts`)的 `routers` 属性出发,这是路由配置的入口锚点,永远有效。
153
+ - **不要依赖固定的文件或目录名**:项目可能将路由写在 `src/main.ts`、`src/router.ts` `src/router/rules/` 目录下,具体位置取决于项目规模。
154
+ - **wok-server 路由使用完整路径作为 key**,不会分散配置,所以搜索路径字符串一定能找到。
116
155
 
117
- ### 场景 3:我要查某个表的结构
156
+ 例如搜索 `/bs/contact/create` 就能在路由文件中找到:
118
157
 
119
- 1. 找 `src/{功能名}/{功能名}.ts`(如 `user/user.ts`)
120
- 2. 查看 `Table` 对象定义(表名、列、ID 字段等)
158
+ ```ts
159
+ '/bs/contact/create': bsContactCreate, // 直接看到 handler 名及其 import 来源
160
+ ```
121
161
 
122
- ### 场景 4:项目可能封装了自定义 Handler
162
+ 路由文件的形态取决于项目规模:
123
163
 
124
- wok-server 内置的 handler 工厂函数(`createJsonHandler`、`createUploadHandler`、`createSseHandler`、`restful`)在真实项目中经常被二次封装。例如:
164
+ - **小型项目**:路由可能直接写在 `src/main.ts`
165
+ - **中型项目**:每个业务模块一个路由文件,通过 `...` 展开符合并导出
125
166
 
126
167
  ```ts
127
- // 项目可能定义了 createAuthJsonHandler,在 createJsonHandler 基础上注入用户信息
128
- export function createAuthJsonHandler<Req, Resp>(opts) {
129
- return createJsonHandler<Req, Resp>({
130
- ...opts,
131
- async handle(body, exchange) {
132
- const auth = new Auth(exchange) // 增加 auth 信息
133
- return opts.handle(body, exchange, auth)
134
- }
135
- })
168
+ // router/rules/contact.ts
169
+ export const contactRouters: Routers = {
170
+ '/bs/contact/create': bsContactCreate,
171
+ '/bs/contact/delete': bsContactDelete,
172
+ '/bs/contact/get': bsContactGet,
173
+ // ...
136
174
  }
137
175
  ```
138
176
 
139
- **面对一个项目时,先搜索以下关键词,判断项目是否有自定义封装:**
140
177
 
141
- - 搜 `createJsonHandler` —— 如果没有匹配,说明项目可能用了自定义封装
142
- - 搜 `create.*Handler` 或 `create.*Json` —— 发现自定义的 handler 工厂函数
143
- - 搜 `from 'wok-server'` 中的 `createJsonHandler` —— 确认哪些文件直接用了内置版本
144
- - 后续搜索接口文件时,用自定义函数名而非内置名
178
+ ## MySQL
145
179
 
146
- ### 场景 5:找不到目标文件?
180
+ ### 通过 SQL 迁移文件了解全局业务
181
+
182
+ SQL 迁移文件记录了完整的建表语句,可以从中了解项目涉及的所有业务表。查找方式如下:
183
+
184
+ 1. 打开项目根目录下的 `.env` 文件
185
+ 2. 查看 `MYSQL_VERSION_CONTROL_ENABLED` 变量是否为 `true`
186
+ 3. 如果为 `true`,再看 `MYSQL_VERSION_CONTROL_DIR` 变量配置的迁移文件目录(如未设置,默认为 `db_migration`)
187
+ 4. 如果 `.env` 不存在或 `MYSQL_VERSION_CONTROL_ENABLED` 不为 `true`,说明项目没有使用版本控制,即没有 SQL 迁移文件
188
+
189
+ 通过迁移文件的建表语句,可以看到每个表的完整字段定义、索引、外键等,比实体 `interface` 更全面,有助于先对项目业务有个大致的了解。
190
+
191
+ ### 通过实体文件查表结构
192
+
193
+ 知道表名查配置,最快的方式是搜索 `tableName`,但不同项目的代码格式化风格可能不同(空格、引号),建议用正则搜索提高容错:
194
+
195
+ ```
196
+ 搜索正则可匹配 tableName\s*:\s*['"]表名['"]
197
+ ```
198
+
199
+ 例如搜索 `tableName:\s*['"]contact['"]`(注意正则中 `\s*` 可以匹配任意数量的空格,`['"]` 可以匹配单引号或双引号),找到匹配位置后,确认满足以下两个条件就是表的配置:
200
+
201
+ 1. 该 `tableName` 所属的常量声明是 `Table<表类型>` 类型,例如 `export const tableContact: Table<Contact>`
202
+ 2. `Table` 导入自 `wok-server`(即 `import { Table } from 'wok-server'`)
203
+
204
+ 满足这两点,就找到了完整的表配置,从中可以看到表名、主键 ID、字段列表、时间戳配置等信息。
205
+
206
+ 示例代码结构:
207
+
208
+ ```ts
209
+ import { Table } from 'wok-server'
210
+
211
+ export interface Contact {
212
+ id: string
213
+ corp_id: string
214
+ name: string
215
+ // ...
216
+ }
217
+
218
+ export const tableContact: Table<Contact> = {
219
+ tableName: 'contact', // ← 搜这个字符串找到这里
220
+ id: 'id', // 主键字段
221
+ columns: ['corp_id', 'name', /* ... */],
222
+ createdDate: { type: 'date', column: 'create_at' },
223
+ updatedDate: { type: 'date', column: 'update_at' }
224
+ }
225
+ ```
147
226
 
148
- 1. **首先检查路由注册** — 搜路由路径字符串,定位 handler 的 import 来源
149
- 2. 全文搜索 `createJsonHandler` —— 列出直接使用内置工厂的 JSON 接口
150
- 3. 全文搜索 `create.*Handler` —— 发现自定义封装的 handler 工厂及使用位置
151
- 4. 全文搜索 `Table<` —— 列出所有数据表定义
152
- 5. 全文搜索 `Interceptor` —— 列出所有拦截器
153
- 6. 全文搜索 `schedule` —— 列出所有定时任务
227
+ 业务逻辑层通常在 `src/{功能名}/{功能名}-service.ts`。
@@ -106,7 +106,6 @@ export const tableUser: Table<User> = {
106
106
 
107
107
  | JS 类型 | MySQL 字段类型 |
108
108
  | :------------- | :------------------------------------------------------------------- |
109
- | `boolean` | TINYINT |
110
109
  | `number` | TINYINT, SMALLINT, INT, MEDIUMINT, YEAR, FLOAT, DOUBLE, BIGINT |
111
110
  | `Date` | TIMESTAMP, DATE, DATETIME |
112
111
  | `Buffer` | TINYBLOB, MEDIUMBLOB, LONGBLOB, BLOB, BINARY, VARBINARY, BIT |
@@ -115,6 +114,21 @@ export const tableUser: Table<User> = {
115
114
 
116
115
  实体字段类型必须与数据库列类型匹配对应的 JS 原生类型,否则查询结果会不正确。可空字段在实体中也定义为可选(`?`)。
117
116
 
117
+ > **⚠️ `boolean` 类型安全警告**
118
+ >
119
+ > MySQL 的 `BOOLEAN` 实际是 `TINYINT(1)`,驱动返回 `0`/`1`(number),**不是** `true`/`false`。
120
+ > 实体类中将字段声明为 `boolean` 会导致 `===` 全等比较出现 bug,且手写 SQL 查询同样存在此问题。
121
+ >
122
+ > 框架**不提供**类型自动映射。最安全的做法是使用 `0 | 1` 类型,与数据库真实返回值保持一致:
123
+ >
124
+ > ```ts
125
+ > export interface User {
126
+ > is_active: 0 | 1 // 推荐
127
+ > }
128
+ > ```
129
+ >
130
+ > `0 | 1` 在条件判断中与 `boolean` 行为一致,仅在 `=== true/false` 时有差异。
131
+
118
132
  ---
119
133
 
120
134
  ## CRUD 操作
@@ -197,17 +211,9 @@ await manager.insertMany(tableUser, [
197
211
  { id: 'im001', nickname: '张飞' },
198
212
  { id: 'im002', nickname: '关羽' }
199
213
  ])
200
-
201
- // 插入时使用表达式(InsertValue)
202
- await manager.insert(tableUser, {
203
- id: 'in002',
204
- nickname: '小红',
205
- balance: ['expr', '?? * ?', ['score', 2]],
206
- createAt: ['now']
207
- })
208
214
  ```
209
215
 
210
- 支持三种表达式:`['now']`(NOW())、`['set', value]`(解决元组冲突)、`['expr', sql, values?]`(自定义 SQL)。
216
+ `insert` 要求传入完整的实体对象,必填字段必须存在,返回的对象是真实的 `T` 类型。
211
217
 
212
218
  ### Upsert
213
219
 
@@ -1,15 +1,17 @@
1
1
  # MySQL 版本管理
2
2
 
3
- 开启 `MYSQL_VERSION_CONTROL_ENABLED=true`,在 `db_migration/` 目录下创建 SQL 文件,文件名为纯数字:
3
+ 开启 `MYSQL_VERSION_CONTROL_ENABLED=true`,在 `db_migration/` 目录下创建 SQL 文件,文件名必须以数字开头,数字后可加描述文字:
4
4
 
5
5
  ```
6
6
  db_migration/
7
- 1.sql # 版本号从 1 开始,必须连续递增
8
- 2.sql
9
- 3.sql
7
+ 1.sql # 等价于版本号 1
8
+ 2_init.sql # 等价于版本号 2
9
+ 3_add_user_index.sql # 等价于版本号 3
10
10
  ```
11
11
 
12
- 文件名必须是纯数字 + `.sql` 后缀(如 `1.sql`、`2.sql`),非数字前缀会导致 `parseInt` 失败而抛异常。编号必须从 1 开始连续递增,不连续也会抛异常。
12
+ 版本号必须从 1 开始连续递增。文件名示例:`1.sql`、`2_init.sql`、`3_add_user_index.sql`。不支持 `v1.sql` `init_v1.sql` 这类不以数字开头的命名。
13
+
14
+ > **⚠️ 重复版本号校验**:如果有两个文件解析出版本号相同(如 `1_init.sql` 和 `1_create_table.sql` 都对应版本 1),启动时会抛异常,并明确指出冲突的文件路径。
13
15
 
14
16
  启动时自动在事务中检测当前版本,顺序执行未执行的 SQL 并更新版本号。
15
17
 
@@ -1,6 +1,5 @@
1
- import { IncomingHttpHeaders, request as requestHttp, Agent as HttpAgent } from 'http'
2
- import { request as requestHttps, Agent as HttpsAgent } from 'https'
3
- import { URL } from 'url'
1
+ import { Agent as HttpAgent, IncomingHttpHeaders, request as requestHttp } from 'http'
2
+ import { Agent as HttpsAgent, request as requestHttps } from 'https'
4
3
 
5
4
  /**
6
5
  * 请求选项.
@@ -141,7 +140,7 @@ export function doRequest(opts: HttpRequestOpts): Promise<HttpResponseInfo> {
141
140
  * @param opts
142
141
  */
143
142
  export async function postJson<T>(
144
- opts: Pick<HttpRequestOpts, 'url' | 'query' | 'headers' | 'timeout'> & { body: any }
143
+ opts: Pick<HttpRequestOpts, 'url' | 'query' | 'headers' | 'timeout' | 'agent'> & { body: unknown }
145
144
  ): Promise<T> {
146
145
  const headers = Object.assign({}, opts.headers || {}, {
147
146
  'Content-Type': 'application/json; charset=utf-8'
@@ -153,7 +152,8 @@ export async function postJson<T>(
153
152
  timeout: opts.timeout,
154
153
  method: 'POST',
155
154
  body: JSON.stringify(opts.body),
156
- followRedirect: false
155
+ followRedirect: false,
156
+ agent: opts.agent
157
157
  })
158
158
  if (res.status !== 200) {
159
159
  if (res.body.byteLength < 1024) {
@@ -178,7 +178,7 @@ export async function postJson<T>(
178
178
  * @param opts
179
179
  */
180
180
  export async function getJson<T>(
181
- opts: Pick<HttpRequestOpts, 'url' | 'query' | 'headers' | 'timeout'>
181
+ opts: Pick<HttpRequestOpts, 'url' | 'query' | 'headers' | 'timeout' | 'agent'>
182
182
  ): Promise<T> {
183
183
  const res = await doRequest({
184
184
  url: opts.url,
@@ -186,7 +186,8 @@ export async function getJson<T>(
186
186
  headers: opts.headers,
187
187
  timeout: opts.timeout,
188
188
  method: 'GET',
189
- followRedirect: true
189
+ followRedirect: true,
190
+ agent: opts.agent
190
191
  })
191
192
  if (res.status !== 200) {
192
193
  if (res.body.byteLength < 1024) {
package/src/lock/index.ts CHANGED
@@ -14,6 +14,9 @@ export interface LockInfo {
14
14
  /**
15
15
  * 锁管理器,主要用于将不确定的顺序且有冲突的异步操作顺序执行,
16
16
  * 防止异步流程庞大穿插执行造成的数据混乱和错误,常见于请求的处理。
17
+ *
18
+ * ⚠️ 注意:这是一个本地内存锁,仅在当前进程内生效,不支持分布式场景。
19
+ * 如果需要在多进程、多服务器环境下使用,请使用 Redis 等分布式锁方案。
17
20
  */
18
21
  class ServerLockManager {
19
22
  /**
@@ -23,7 +26,7 @@ class ServerLockManager {
23
26
 
24
27
  constructor() {
25
28
  // 定期清理,将过期的信息移除,防止内存泄漏
26
- setTimeout(() => {
29
+ setInterval(() => {
27
30
  const keysToBeDeleted: string[] = []
28
31
  const now = Date.now()
29
32
  for (const entry of this.lockMap.entries()) {
@@ -144,7 +147,7 @@ class ServerLockManager {
144
147
  */
145
148
  private sleep() {
146
149
  return new Promise<void>((resolve, reject) => {
147
- setTimeout(resolve, 0)
150
+ setTimeout(resolve, 50)
148
151
  })
149
152
  }
150
153
  }
package/src/mvc/config.ts CHANGED
@@ -41,6 +41,11 @@ export interface WebConfig {
41
41
  * pem 格式证书私钥文件路径
42
42
  */
43
43
  tlsKey: string
44
+ /**
45
+ * 请求体最大大小(字节),超出返回 413 Payload Too Large。
46
+ * 默认 10MB,设置为 0 表示不限制。
47
+ */
48
+ maxBodySize: number
44
49
  }
45
50
 
46
51
  export function getConfig() {
@@ -54,7 +59,8 @@ export function getConfig() {
54
59
  corsAllowOrigin: '*',
55
60
  tlsEnable: false,
56
61
  tlsKey: '',
57
- tlsCert: ''
62
+ tlsCert: '',
63
+ maxBodySize: 10 * 1024 * 1024
58
64
  },
59
65
  'SERVER',
60
66
  {
@@ -64,7 +70,8 @@ export function getConfig() {
64
70
  corsAllowOrigin: [notBlank()],
65
71
  corsAllowHeaders: [notBlank()],
66
72
  corsAllowMethods: [notBlank()],
67
- tlsEnable: [notNull()]
73
+ tlsEnable: [notNull()],
74
+ maxBodySize: [notNull(), min(0)]
68
75
  }
69
76
  )
70
77
  }