wok-server 0.6.0 → 0.7.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 (41) hide show
  1. package/README.en.md +1 -1
  2. package/dist/mysql/config.js +1 -1
  3. package/dist/mysql/manager/base.js +39 -0
  4. package/dist/mysql/manager/ops/criteria.js +25 -0
  5. package/dist/mysql/manager/ops/delete.js +4 -10
  6. package/dist/mysql/manager/ops/find.js +10 -30
  7. package/dist/mysql/manager/ops/index.js +2 -0
  8. package/dist/mysql/manager/ops/insert.js +39 -13
  9. package/dist/mysql/manager/ops/order-by.js +28 -0
  10. package/dist/mysql/manager/ops/paginate.js +26 -1
  11. package/dist/mysql/manager/ops/update.js +43 -37
  12. package/dist/mysql/manager/ops/upsert.js +178 -0
  13. package/dist/mysql/manager/ops/utils.js +4 -0
  14. package/documentation/en/mysql.md +135 -5
  15. package/documentation/zh-cn/mysql.md +146 -17
  16. package/package.json +2 -1
  17. package/skills/wok-server-code-navigation/SKILL.md +153 -0
  18. package/skills/wok-server-mysql/SKILL.md +76 -3
  19. package/src/mysql/config.ts +2 -2
  20. package/src/mysql/manager/base.ts +51 -4
  21. package/src/mysql/manager/ops/criteria.ts +34 -0
  22. package/src/mysql/manager/ops/delete.ts +5 -10
  23. package/src/mysql/manager/ops/find.ts +12 -29
  24. package/src/mysql/manager/ops/index.ts +2 -0
  25. package/src/mysql/manager/ops/insert.ts +53 -15
  26. package/src/mysql/manager/ops/order-by.ts +58 -0
  27. package/src/mysql/manager/ops/paginate.ts +42 -2
  28. package/src/mysql/manager/ops/update.ts +66 -42
  29. package/src/mysql/manager/ops/upsert.ts +224 -0
  30. package/src/mysql/manager/ops/utils.ts +4 -0
  31. package/types/mysql/config.d.ts +1 -1
  32. package/types/mysql/manager/base.d.ts +35 -4
  33. package/types/mysql/manager/ops/criteria.d.ts +10 -0
  34. package/types/mysql/manager/ops/delete.d.ts +2 -1
  35. package/types/mysql/manager/ops/find.d.ts +3 -2
  36. package/types/mysql/manager/ops/index.d.ts +2 -0
  37. package/types/mysql/manager/ops/insert.d.ts +16 -2
  38. package/types/mysql/manager/ops/order-by.d.ts +38 -0
  39. package/types/mysql/manager/ops/paginate.d.ts +18 -1
  40. package/types/mysql/manager/ops/update.d.ts +26 -3
  41. package/types/mysql/manager/ops/upsert.d.ts +36 -0
@@ -0,0 +1,153 @@
1
+ ---
2
+ name: wok-server-code-navigation
3
+ description: 教你如何读懂一个 wok-server 项目的代码结构,快速定位目标文件。
4
+ license: MIT
5
+ metadata:
6
+ author: Peak Tai
7
+ email: peaktai@qq.com
8
+ ---
9
+
10
+ # wok-server 项目代码导航
11
+
12
+ ## 概述
13
+
14
+ 面对一个 wok-server 项目时,按以下路径快速理解项目结构、定位目标代码。核心原则:**项目按功能划分目录,路由集中配置,每个接口/拦截器独立一个文件**。
15
+
16
+ ## 项目入口 — main.ts
17
+
18
+ 入口文件在 `src/main.ts`,包含:
19
+
20
+ 1. 全局初始化(如 `Date.prototype.toJSON`、`enableMysql()`)
21
+ 2. `startWebServer()` 调用,配置 routers 和 interceptors
22
+ 3. 任务调度(`scheduleWithFixedDelay` 等)
23
+
24
+ **所以 `main.ts` 是读懂项目的第一个文件**。从这里可以看到所有路由映射和拦截器链。
25
+
26
+ ## 路由定位决策树
27
+
28
+ ```
29
+ 要找一个接口的处理逻辑?
30
+ → 打开 src/main.ts(或 src/router.ts)
31
+ → 找到路由对象中对应路径的 key
32
+ → 看它的 value(通常是 import 的 handler 函数)
33
+ → Ctrl/Cmd + 点击跳转到 handler 文件
34
+ ```
35
+
36
+ 路由集中在一个地方,所以**搜路由路径就能找到对应 handler 的 import**。
37
+
38
+ ## 文件定位速查表
39
+
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` |
49
+
50
+ ## 目录结构速览
51
+
52
+ ```
53
+ 根目录
54
+ ├── db_migration/ # MySQL 迁移文件
55
+ ├── 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
71
+ ```
72
+
73
+ ## 阅读项目的步骤
74
+
75
+ ### 第一步:看 main.ts
76
+
77
+ 打开 `src/main.ts`,关注三个区域:
78
+
79
+ 1. **顶部 import** — 了解项目用了哪些 wok-server 功能模块(MySQL?MongoDB?Task?)
80
+ 2. **startWebServer() 调用** — 了解有哪些路由和拦截器
81
+ 3. **任务调度调用** — 了解有哪些定时任务
82
+
83
+ ### 第二步:看路由表
84
+
85
+ 从 `main.ts` 的 routers 对象中找到你关心的路径,跳转到对应 handler。
86
+
87
+ ### 第三步:看拦截器链
88
+
89
+ 从 `main.ts` 的 interceptors 数组了解请求处理流程。典型链路:
90
+
91
+ ```
92
+ 请求 → globalErrorInterceptor → authInterceptor → handler
93
+ ```
94
+
95
+ ### 第四步:按需看业务目录
96
+
97
+ 根据 handler 文件所在的目录,查看同目录下的:
98
+
99
+ - `{功能}.ts` — 了解数据实体结构
100
+ - `{功能}-interceptor.ts` — 了解功能级拦截逻辑
101
+ - 其他 handler 文件 — 了解该功能的完整接口
102
+
103
+ ## 常见查找场景
104
+
105
+ ### 场景 1:我要改一个接口的逻辑
106
+
107
+ 1. 从路由路径反查 handler 文件(搜路径字符串)
108
+ 2. 打开 handler 文件,找到 `handle()` 函数体
109
+ 3. 修改逻辑
110
+
111
+ ### 场景 2:我要新增一个接口
112
+
113
+ 1. 在对应功能目录下新建 `xxx.ts`
114
+ 2. 用 `createJsonHandler` 或其他工厂函数创建 handler
115
+ 3. 在 `src/main.ts` 或 `src/router.ts` 中注册路由
116
+
117
+ ### 场景 3:我要查某个表的结构
118
+
119
+ 1. 找 `src/{功能名}/{功能名}.ts`(如 `user/user.ts`)
120
+ 2. 查看 `Table` 对象定义(表名、列、ID 字段等)
121
+
122
+ ### 场景 4:项目可能封装了自定义 Handler
123
+
124
+ wok-server 内置的 handler 工厂函数(`createJsonHandler`、`createUploadHandler`、`createSseHandler`、`restful`)在真实项目中经常被二次封装。例如:
125
+
126
+ ```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
+ })
136
+ }
137
+ ```
138
+
139
+ **面对一个项目时,先搜索以下关键词,判断项目是否有自定义封装:**
140
+
141
+ - 搜 `createJsonHandler` —— 如果没有匹配,说明项目可能用了自定义封装
142
+ - 搜 `create.*Handler` 或 `create.*Json` —— 发现自定义的 handler 工厂函数
143
+ - 搜 `from 'wok-server'` 中的 `createJsonHandler` —— 确认哪些文件直接用了内置版本
144
+ - 后续搜索接口文件时,用自定义函数名而非内置名
145
+
146
+ ### 场景 5:找不到目标文件?
147
+
148
+ 1. **首先检查路由注册** — 搜路由路径字符串,定位 handler 的 import 来源
149
+ 2. 全文搜索 `createJsonHandler` —— 列出直接使用内置工厂的 JSON 接口
150
+ 3. 全文搜索 `create.*Handler` —— 发现自定义封装的 handler 工厂及使用位置
151
+ 4. 全文搜索 `Table<` —— 列出所有数据表定义
152
+ 5. 全文搜索 `Interceptor` —— 列出所有拦截器
153
+ 6. 全文搜索 `schedule` —— 列出所有定时任务
@@ -139,7 +139,14 @@ const user = await manager.findFirst(tableUser, c =>
139
139
  )
140
140
  ```
141
141
 
142
- **链式条件方法**:`eq`、`neq`、`gt`、`gte`、`lt`、`lte`、`like`、`notLike`、`between`、`in`、`notIn`、`isNull`、`isNotNull`、`or`、`and`。
142
+ **链式条件方法**:`eq`、`neq`、`gt`、`gte`、`lt`、`lte`、`like`、`notLike`、`between`、`in`、`notIn`、`isNull`、`isNotNull`、`or`、`and`、`expr`。
143
+
144
+ `expr()` 方法支持在 WHERE 子句中插入自定义 SQL 表达式:
145
+
146
+ ```ts
147
+ c => c.like('id', 'critex%').expr('?? * ? > ?', ['balance', 2, 50])
148
+ // SQL: where `id` like 'critex%' and `balance` * 2 > 50
149
+ ```
143
150
 
144
151
  ### 复杂查询
145
152
 
@@ -151,6 +158,14 @@ const list = await manager.find({
151
158
  limit: 10,
152
159
  orderBy: [['balance', 'asc']]
153
160
  })
161
+
162
+ // 自定义排序表达式
163
+ const list2 = await manager.find({
164
+ table: tableUser,
165
+ criteria: c => c.like('nickname', 'ob%'),
166
+ orderBy: [['expr', '?? * ?', ['balance', 2], 'desc']]
167
+ })
168
+ // SQL: ORDER BY `balance` * 2 desc
154
169
  ```
155
170
 
156
171
  ### 分页
@@ -163,6 +178,15 @@ const page = await manager.paginate({
163
178
  orderBy: [['balance', 'asc'], ['id', 'asc']]
164
179
  })
165
180
  // { total: number, list: T[] }
181
+
182
+ // 指定字段分页查询
183
+ const page2 = await manager.paginateSelect({
184
+ table: tableUser,
185
+ criteria: c => c.like('id', 'pg0%'),
186
+ select: ['id', 'nickname', 'balance'],
187
+ pn: 2, pz: 5
188
+ })
189
+ // { total: number, list: Pick<T, 'id'|'nickname'|'balance'>[] }
166
190
  ```
167
191
 
168
192
  ### 插入
@@ -173,6 +197,36 @@ await manager.insertMany(tableUser, [
173
197
  { id: 'im001', nickname: '张飞' },
174
198
  { id: 'im002', nickname: '关羽' }
175
199
  ])
200
+
201
+ // 插入时使用表达式(InsertValue)
202
+ await manager.insert(tableUser, {
203
+ id: 'in002',
204
+ nickname: '小红',
205
+ balance: ['expr', '?? * ?', ['score', 2]],
206
+ createAt: ['now']
207
+ })
208
+ ```
209
+
210
+ 支持三种表达式:`['now']`(NOW())、`['set', value]`(解决元组冲突)、`['expr', sql, values?]`(自定义 SQL)。
211
+
212
+ ### Upsert
213
+
214
+ ```ts
215
+ // 单条 upsert,主键冲突则更新
216
+ await manager.upsert(tableUser, { id: 'us001', nickname: '赵云', balance: 10 })
217
+
218
+ // 批量 upsert
219
+ await manager.upsertMany(tableUser, [
220
+ { id: 'us002', nickname: '马超', balance: 20 },
221
+ { id: 'us003', nickname: '黄忠', balance: 30 }
222
+ ])
223
+
224
+ // 冲突时自定义更新(使用 Updater)
225
+ await manager.upsertWithUpdater(
226
+ tableUser,
227
+ { id: 'us001', nickname: '赵云', balance: 10 },
228
+ { balance: ['inc', 5], nickname: '赵云-updated' }
229
+ )
176
230
  ```
177
231
 
178
232
  ### 更新
@@ -181,13 +235,28 @@ await manager.insertMany(tableUser, [
181
235
  // 完整更新(需要完整文档)
182
236
  await manager.update(tableUser, { id: 'xxx', nickname: '王五' })
183
237
 
184
- // 局部更新(使用元组语法:['inc', 22] 表示 +22,[null] 表示置空)
185
- await manager.partialUpdate(tableUser, { id: 'pu000', balance: ['inc', 22] })
238
+ // 局部更新
239
+ await manager.partialUpdate(tableUser, {
240
+ id: 'pu000',
241
+ // 自增 +22
242
+ balance: ['inc', 22],
243
+ // 自增 +1(默认值)
244
+ visits: ['inc'],
245
+ // NULL 安全的字符串追加
246
+ nickname: ['concat', '-suffix'],
247
+ // 设置为 NOW()
248
+ last_login_at: ['now'],
249
+ // 自定义表达式
250
+ score: ['expr', '?? * ?', ['score', 2]]
251
+ })
186
252
 
187
253
  // 批量更新
188
254
  await manager.updateMany(tableUser, c => c.like('nickname', 'um%'), { balance: ['inc', 2] })
189
255
  ```
190
256
 
257
+ > **0.7.0 版本**开始,`null` 不再自动置 NULL。如需将字段设置为 NULL,必须显式使用 `['setNull']`。
258
+ > `['func']` 已移除,请使用 `['expr']` 替代(如 `['func', 'NOW()']` → `['expr', 'NOW()']`)。
259
+
191
260
  ### 删除
192
261
 
193
262
  ```ts
@@ -225,8 +294,12 @@ const affected = await manager.modify(`update user set nickname='无名' where n
225
294
  | `existsById` | id 判断存在 | |
226
295
  | `count` | 统计数量 | ⚠️ |
227
296
  | `paginate` | 分页查询 | ⚠️ |
297
+ | `paginateSelect` | 指定字段分页查询 | ⚠️ |
228
298
  | `insert` | 插入单条 | |
229
299
  | `insertMany` | 批量插入 | |
300
+ | `upsert` | 插入单条,主键冲突则更新 | |
301
+ | `upsertMany` | 批量 upsert | |
302
+ | `upsertWithUpdater` | upsert 单条,冲突时自定义更新 | |
230
303
  | `update` | 完整更新 | |
231
304
  | `partialUpdate`| 局部更新 | |
232
305
  | `updateOne` | 更新第一条相等条件 | |
@@ -57,7 +57,7 @@ export interface MysqlConfig {
57
57
  */
58
58
  maxIdle: number
59
59
  /**
60
- * 闲置的超时时间,也即多久不用算闲置,单位毫秒
60
+ * 闲置的超时时间,当一个数据库连接不再被业务使用、处于闲置状态的时间超过这个阈值,连接池就会自动回收并关闭这个连接,释放资源。
61
61
  */
62
62
  idleTimeout: number
63
63
  /**
@@ -123,7 +123,7 @@ export const configValidation: ValidationOpts<MysqlConfig> = {
123
123
  maxIdle: [notNull(), min(1), max(999)],
124
124
  idleTimeout: [notNull(), min(1000), max(60000)],
125
125
  slowSqlWarn: [notNull()],
126
- slowSqlMs: [notNull(), min(1), max(3600000)],
126
+ slowSqlMs: [notNull(), min(0), max(3600000)],
127
127
  transactionTimeout: [notNull(), min(0), max(60000)],
128
128
  transactionStrict: [notNull()],
129
129
  maxOpsInStrictTx: [notNull(), min(1)]
@@ -8,8 +8,11 @@ import {
8
8
  MixCriteria,
9
9
  MysqlPage,
10
10
  MysqlPaginateOpts,
11
+ MysqlPaginateSelectOpts,
12
+ OrderBy,
11
13
  UpdateOpts,
12
14
  Updater,
15
+ InsertValue,
13
16
  count,
14
17
  deleteMany,
15
18
  deleteById,
@@ -24,13 +27,17 @@ import {
24
27
  insertMany,
25
28
  modify,
26
29
  paginate,
30
+ paginateSelect,
27
31
  partialUpdate,
28
32
  query,
29
33
  update,
30
34
  updateMany,
31
35
  updateOne,
32
36
  FindSelectOpts,
33
- findSelect
37
+ findSelect,
38
+ upsert,
39
+ upsertMany,
40
+ upsertWithUpdater
34
41
  } from './ops'
35
42
  import { promiseGetConnection } from './utils'
36
43
 
@@ -162,7 +169,7 @@ export abstract class BaseMysqlManager {
162
169
  findFirst<T>(
163
170
  table: Table<T>,
164
171
  criteria?: MixCriteria<T>,
165
- orderBy?: Array<[keyof T, 'asc' | 'desc']>
172
+ orderBy?: OrderBy<T>
166
173
  ): Promise<T | null> {
167
174
  return this.queryWithConnection(conn =>
168
175
  findFirst(this.opts.config, conn, table, criteria, orderBy)
@@ -175,7 +182,7 @@ export abstract class BaseMysqlManager {
175
182
  * @param data 数据,数据必须是 T 的实例, T 必须是已配置的实体类类型,否则无法完成操作
176
183
  * @returns 插入后的数据
177
184
  */
178
- insert<T>(table: Table<T>, data: T): Promise<T> {
185
+ insert<T>(table: Table<T>, data: InsertValue<T>): Promise<T> {
179
186
  return this.queryWithConnection(conn => insert(this.opts.config, conn, table, data))
180
187
  }
181
188
  /**
@@ -183,9 +190,40 @@ export abstract class BaseMysqlManager {
183
190
  * @param table 表
184
191
  * @param list 要插入的数据列表
185
192
  */
186
- insertMany<T>(table: Table<T>, list: T[]): Promise<void> {
193
+ insertMany<T>(table: Table<T>, list: InsertValue<T>[]): Promise<void> {
187
194
  return this.queryWithConnection(conn => insertMany(this.opts.config, conn, table, list))
188
195
  }
196
+ /**
197
+ * Upsert 单条数据
198
+ * 如果主键冲突则更新,否则插入
199
+ * @param table 表信息
200
+ * @param data 数据
201
+ * @returns
202
+ */
203
+ upsert<T>(table: Table<T>, data: InsertValue<T>): Promise<T> {
204
+ return this.queryWithConnection(conn => upsert(this.opts.config, conn, table, data))
205
+ }
206
+ /**
207
+ * Upsert 多条数据
208
+ * 如果主键冲突则更新,否则插入
209
+ * @param table 表
210
+ * @param list 要插入的数据列表
211
+ * @returns 影响的行数
212
+ */
213
+ upsertMany<T>(table: Table<T>, list: InsertValue<T>[]): Promise<number> {
214
+ return this.queryWithConnection(conn => upsertMany(this.opts.config, conn, table, list))
215
+ }
216
+ /**
217
+ * Upsert 单条数据(支持自定义更新器)
218
+ * 如果主键冲突则按自定义逻辑更新,否则插入
219
+ * @param table 表信息
220
+ * @param data 插入的数据
221
+ * @param updater 冲突时的更新器
222
+ * @returns
223
+ */
224
+ upsertWithUpdater<T>(table: Table<T>, data: InsertValue<T>, updater: Updater<T>): Promise<T> {
225
+ return this.queryWithConnection(conn => upsertWithUpdater(this.opts.config, conn, table, data, updater))
226
+ }
189
227
  /**
190
228
  * 更新
191
229
  * @param table 表信息
@@ -264,6 +302,15 @@ export abstract class BaseMysqlManager {
264
302
  return this.queryWithConnection(conn => paginate(this.opts.config, conn, opts))
265
303
  }
266
304
 
305
+ /**
306
+ * 指定字段分页查询
307
+ * @param opts
308
+ * @returns
309
+ */
310
+ paginateSelect<T, K extends keyof T>(opts: MysqlPaginateSelectOpts<T, K>): Promise<MysqlPage<Pick<T, K>>> {
311
+ return this.queryWithConnection(conn => paginateSelect(this.opts.config, conn, opts))
312
+ }
313
+
267
314
  /**
268
315
  * 自定义查询,指定 sql 、参数和返回值类型
269
316
  * @param sql 预编译 sql ,参数使用 ”?“(英文问号) 占位,注意查询的字段名称会与返回值类型的字段映射,如果 sql 中的字段名称很特殊(比如纯数字等),需要设置别名,避免产生映射错误
@@ -53,12 +53,21 @@ interface Criterion<T> {
53
53
  | 'isNotNull'
54
54
  | 'notLike'
55
55
  | 'between'
56
+ | 'expr'
56
57
  key?: MysqlCriteriaKey<T>
57
58
  value?: any
58
59
  /**
59
60
  * 嵌套的其它查询, or 和 and 条件下有效
60
61
  */
61
62
  criteria?: MysqlCriteria<T>
63
+ /**
64
+ * 自定义表达式的 SQL 片段(expr 条件下有效)
65
+ */
66
+ exprSql?: string
67
+ /**
68
+ * 自定义表达式的参数值
69
+ */
70
+ exprValues?: any[]
62
71
  }
63
72
 
64
73
  /**
@@ -208,6 +217,19 @@ export class MysqlCriteria<T> {
208
217
  this.criteria.push({ type: 'isNotNull', key: field })
209
218
  return this
210
219
  }
220
+ /**
221
+ * 自定义表达式查询
222
+ * 如 .expr('?? * ? > ?', ['balance', 2, 50])
223
+ * 如 .expr('MATCH(??, ??) AGAINST(? IN BOOLEAN MODE)', ['title', 'content', keyword])
224
+ * 如 .expr('VECTOR_DISTANCE(??, STRING_TO_VECTOR(?)) < ?', ['content_vec', embedding, threshold])
225
+ * @param sql SQL 片段,使用 ?? 引用列名,? 引用参数值
226
+ * @param values 参数值数组,按 SQL 中占位符顺序传入
227
+ * @returns
228
+ */
229
+ expr(sql: string, values?: any[]) {
230
+ this.criteria.push({ type: 'expr', exprSql: sql, exprValues: values || [] })
231
+ return this
232
+ }
211
233
  /**
212
234
  * 判定是否空,未设置条件.
213
235
  * @returns
@@ -220,6 +242,12 @@ export class MysqlCriteria<T> {
220
242
  */
221
243
  check() {
222
244
  for (const criterion of this.criteria) {
245
+ if (criterion.type === 'expr') {
246
+ if (!criterion.exprSql) {
247
+ throw new MysqlException('expr clause exprSql cannot be empty')
248
+ }
249
+ continue
250
+ }
223
251
  if (criterion.type === 'or' || criterion.type === 'and') {
224
252
  if (!criterion.criteria) {
225
253
  throw new MysqlException(`${criterion.type} clause cannot be empty`)
@@ -357,6 +385,12 @@ export class MysqlCriteria<T> {
357
385
  continue
358
386
  }
359
387
  }
388
+ // 自定义表达式
389
+ else if (criterion.type === 'expr' && criterion.exprSql) {
390
+ sqlFragments.push(`and ${criterion.exprSql} `)
391
+ values.push(...(criterion.exprValues || []))
392
+ continue
393
+ }
360
394
  }
361
395
  if (!sqlFragments.length) {
362
396
  throw new MysqlException('No valid query criteria have been set.')
@@ -4,6 +4,7 @@ import { Table } from '../../table-info'
4
4
  import { promiseQuery } from '../utils'
5
5
  import { MixCriteria, buildQuery } from './criteria'
6
6
  import { MysqlConfig } from '../../config'
7
+ import { OrderBy, buildOrderBy } from './order-by'
7
8
 
8
9
  /**
9
10
  * 按 id 删除.
@@ -49,7 +50,7 @@ export interface DeleteManyOpts<T> {
49
50
  /**
50
51
  * 排序规则,按先后顺序放入,每个规则是一个元组,第一个元素是字段名称,第二个元素是顺序
51
52
  */
52
- orderBy?: Array<[keyof T, 'asc' | 'desc']>
53
+ orderBy?: OrderBy<T>
53
54
  }
54
55
 
55
56
  /**
@@ -77,15 +78,9 @@ export async function deleteMany<T>(
77
78
  }
78
79
  // 排序
79
80
  if (opts.orderBy && opts.orderBy.length) {
80
- opts.orderBy.forEach((orderBy, idx) => {
81
- const [field, sort] = orderBy
82
- if (idx == 0) {
83
- sql += ` order by ?? ${sort} `
84
- } else {
85
- sql += ` , ?? ${sort} `
86
- }
87
- values.push(field)
88
- })
81
+ const ob = buildOrderBy(opts.orderBy)
82
+ sql += ob.sql
83
+ values.push(...ob.values)
89
84
  }
90
85
  // 数量限制
91
86
  if (opts.limit) {
@@ -3,6 +3,7 @@ import { Table } from '../../table-info'
3
3
  import { MixCriteria, buildQuery } from './criteria'
4
4
  import { promiseQuery } from '../utils'
5
5
  import { MysqlConfig } from '../../config'
6
+ import { OrderBy, buildOrderBy } from './order-by'
6
7
 
7
8
  /**
8
9
  * 按 id 查询
@@ -67,7 +68,7 @@ export interface FindOpts<T> {
67
68
  /**
68
69
  * 排序规则,按先后顺序放入,每个规则是一个元组,第一个元素是字段名称,第二个元素是顺序
69
70
  */
70
- orderBy?: Array<[keyof T, 'asc' | 'desc']>
71
+ orderBy?: OrderBy<T>
71
72
  }
72
73
  /**
73
74
  * 条件查询
@@ -89,15 +90,9 @@ export async function find<T>(
89
90
  }
90
91
  // 排序
91
92
  if (opts.orderBy && opts.orderBy.length) {
92
- opts.orderBy.forEach((orderBy, idx) => {
93
- const [field, sort] = orderBy
94
- if (idx == 0) {
95
- sql += ` order by ?? ${sort} `
96
- } else {
97
- sql += ` , ?? ${sort} `
98
- }
99
- values.push(field)
100
- })
93
+ const ob = buildOrderBy(opts.orderBy)
94
+ sql += ob.sql
95
+ values.push(...ob.values)
101
96
  }
102
97
  // 数量限制
103
98
  if (opts.limit) {
@@ -141,15 +136,9 @@ export async function findSelect<T, K extends keyof T>(
141
136
  }
142
137
  // 排序
143
138
  if (opts.orderBy && opts.orderBy.length) {
144
- opts.orderBy.forEach((orderBy, idx) => {
145
- const [field, sort] = orderBy
146
- if (idx == 0) {
147
- sql += ` order by ?? ${sort} `
148
- } else {
149
- sql += ` , ?? ${sort} `
150
- }
151
- values.push(field)
152
- })
139
+ const ob = buildOrderBy(opts.orderBy)
140
+ sql += ob.sql
141
+ values.push(...ob.values)
153
142
  }
154
143
  // 数量限制
155
144
  if (opts.limit) {
@@ -198,7 +187,7 @@ export async function findFirst<T>(
198
187
  /**
199
188
  * 排序规则,按先后顺序放入,每个规则是一个元组,第一个元素是字段名称,第二个元素是顺序
200
189
  */
201
- orderBy?: Array<[keyof T, 'asc' | 'desc']>
190
+ orderBy?: OrderBy<T>
202
191
  ): Promise<T | null> {
203
192
  let query = criteria ? buildQuery(criteria) : undefined
204
193
  let sql = `select * from ?? `
@@ -209,15 +198,9 @@ export async function findFirst<T>(
209
198
  }
210
199
  // 排序
211
200
  if (orderBy && orderBy.length) {
212
- orderBy.forEach((orderBy, idx) => {
213
- const [field, sort] = orderBy
214
- if (idx == 0) {
215
- sql += ` order by ?? ${sort} `
216
- } else {
217
- sql += ` , ?? ${sort} `
218
- }
219
- values.push(field)
220
- })
201
+ const ob = buildOrderBy(orderBy)
202
+ sql += ob.sql
203
+ values.push(...ob.values)
221
204
  }
222
205
  sql += ' limit 1'
223
206
  const res = await promiseQuery(config, conn, sql, values)
@@ -6,6 +6,8 @@ export * from './exist'
6
6
  export * from './find'
7
7
  export * from './insert'
8
8
  export * from './modify'
9
+ export * from './order-by'
10
+ export * from './upsert'
9
11
  export * from './paginate'
10
12
  export * from './query'
11
13
  export * from './update'