wok-server 0.7.2 → 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 (41) hide show
  1. package/README.md +101 -14
  2. package/dist/http-client/index.js +1 -2
  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 +0 -1
  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/lock/index.d.ts +3 -0
  37. package/types/mvc/config.d.ts +5 -0
  38. package/types/mvc/exchange.d.ts +13 -3
  39. package/types/mysql/manager/base.d.ts +12 -12
  40. package/types/mysql/manager/ops/insert.d.ts +2 -16
  41. package/types/mysql/manager/ops/upsert.d.ts +3 -4
package/README.md CHANGED
@@ -15,15 +15,6 @@
15
15
  - **数据库** — MySQL、MongoDB
16
16
  - **其他** — 周期任务
17
17
 
18
- ## 为什么选择 Wok Server
19
-
20
- - 函数式为主,少量面向对象,学习成本低
21
- - 保持克制,不引入代理、装饰器等增强技术
22
- - 轻量封装,兼容第三方 HTTP 生态
23
- - 完整的类型定义,配合 IDE 智能补全,开发效率高
24
- - 注释即文档,方法及参数均有详细说明
25
- - 内置国际化支持,可扩展多语言
26
-
27
18
  ## 快速开始
28
19
 
29
20
  ```bash
@@ -61,12 +52,108 @@ npx skills add peaktai/wok-server --all
61
52
  npx skills add https://gitee.com/tai/wok-server.git --all
62
53
  ```
63
54
 
64
- ## FAQ
55
+ ## 为什么选择 Wok Server
56
+
57
+ 核心原则只有一个:**实用主义**。每一个设计决策,都是在"简单透明"和"功能强大"之间选择了前者——不是因为做不到,而是因为**选择不做**。
58
+
59
+ ### 纯对象配置,没有魔法
60
+
61
+ 实体和数据库的映射就是一个普通对象,不需要学习装饰器、反射或任何框架特有的语法:
62
+
63
+ ```ts
64
+ export interface User {
65
+ id: string
66
+ name: string
67
+ }
68
+
69
+ export const tableUser: Table<User> = {
70
+ tableName: 'user',
71
+ id: 'id',
72
+ columns: ['name']
73
+ }
74
+ ```
75
+
76
+ 所有信息一目了然,出问题直接看这一个文件。不需要理解隐式的元数据系统,AI 和人类都容易理解。
77
+
78
+ ### 显式优于隐式
79
+
80
+ 读写分离、数据源选择都由代码显式决定,不会出现"写后读到旧数据"的意外:
81
+
82
+ ```ts
83
+ const masterMgr = getMysqlManager('master')
84
+ const slaveMgr = getMysqlManager('slave')
85
+
86
+ // 写操作显式走主库
87
+ await masterMgr.insert(tableUser, { id: '001', nickname: 'jack' })
88
+
89
+ // 读操作显式走从库
90
+ const user = await slaveMgr.findById(tableUser, '001')
91
+ ```
92
+
93
+ 代码多一点,但完全可控。调试时清晰知道 SQL 发到了哪里。
94
+
95
+ ### 纯 SQL 迁移,零学习成本
96
+
97
+ 迁移就是按顺序执行的 SQL 文件,框架只负责"按顺序执行 + 版本记录":
98
+
99
+ ```sql
100
+ -- 001_init.sql
101
+ CREATE TABLE `user` (
102
+ `id` varchar(32) NOT NULL PRIMARY KEY,
103
+ `nickname` varchar(100) DEFAULT NULL
104
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
105
+
106
+ -- 002_add_age.sql
107
+ ALTER TABLE `user` ADD COLUMN `age` int DEFAULT NULL AFTER `nickname`;
108
+ ```
109
+
110
+ 会 SQL 就会写迁移,完全透明,DBA 可直接审查,任何数据库特性都能用。
111
+
112
+ ### 统一且直觉的 API
113
+
114
+ 查询 API 遵循统一模式,学会一个,其他都是同一模式:
115
+
116
+ | 操作 | 单条 | 列表 | 分页 | 部分字段 |
117
+ |------|------|------|------|---------|
118
+ | 查询 | `findFirst` | `find` | `paginate` | `findSelect` / `paginateSelect` |
119
+
120
+ 条件查询用链式方法,不需要记忆操作符类名:
121
+
122
+ ```ts
123
+ await mgr.findFirst(tableUser, c =>
124
+ c.like('name', 'Bob%').gt('age', 18).between('score', 60, 100)
125
+ )
126
+ ```
127
+
128
+ 方法名就是自然语言,不需要 import 任何东西。
129
+
130
+ ### 简单直接的类型系统
131
+
132
+ 类型是约束工具,不是智力游戏:
133
+
134
+ ```ts
135
+ // findById 返回 User | undefined
136
+ const user = await mgr.findById(tableUser, '001')
137
+
138
+ // insert 参数自动处理可选字段和元组表达式
139
+ await mgr.insert(tableUser, { id: '001', name: 'Bob' })
140
+
141
+ // findSelect 返回 Pick<User, K>
142
+ const list = await mgr.findSelect(tableUser, ['id', 'name'])
143
+ ```
144
+
145
+ 没有复杂的条件类型、映射类型体操或 `DeepPartial` 等抽象概念。用简单的泛型参数实现类型安全,配合 IDE 智能补全,开发效率高。
146
+
147
+ ### 函数式为主,学习成本低
65
148
 
66
- ### 为什么内置 MySQL 和 MongoDB 驱动?
149
+ 以函数式风格为主,少量面向对象,保持克制,不引入代理、装饰器等增强技术。轻量封装,兼容第三方 HTTP 生态。注释即文档,方法及参数均有详细说明。内置国际化支持,可扩展多语言。
67
150
 
68
- mysql2 mongodb 驱动已直接集成,安装一个包即可使用,避免多个包的版本冲突和维护成本。对于后端应用,包体积的增加影响有限。
151
+ ### AI 编程友好
69
152
 
70
- ### 为什么 WebSocket、文件上传等功能需要自行引入?
153
+ wok-server 的简单透明设计,在 AI 辅助编程时代有天然优势:
71
154
 
72
- 这些功能基于 Node.js 原生 `http` 模块,天然兼容框架,无需专门适配。由用户按需引入第三方库,可以充分利用社区生态,同时减少框架的维护负担。
155
+ - 纯对象配置 = AI 容易理解上下文,不需要理解框架特有的元数据系统
156
+ - 显式调用 = AI 从代码本身就能理解意图,不需要追踪隐式的配置
157
+ - 纯 SQL 迁移 = AI 直接生成可执行代码,没有生成步骤、没有交互、没有黑盒转换
158
+ - 无关联 ORM 自动加载 = AI 写 join 时必须显式写 SQL,性能问题更容易从 SQL 本身识别
159
+ - 统一的元组表达式 = AI 学习一次模式,到处复用,不容易混淆
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getJson = exports.postJson = exports.doRequest = void 0;
4
4
  const http_1 = require("http");
5
5
  const https_1 = require("https");
6
- const url_1 = require("url");
7
6
  /**
8
7
  * 发送 http 请求.
9
8
  * @param opts
@@ -11,7 +10,7 @@ const url_1 = require("url");
11
10
  */
12
11
  function doRequest(opts) {
13
12
  return new Promise((resolve, reject) => {
14
- const url = new url_1.URL(opts.url);
13
+ const url = new URL(opts.url);
15
14
  // query
16
15
  if (opts.query) {
17
16
  Object.entries(opts.query).forEach(entry => {
@@ -5,6 +5,9 @@ const crypto_1 = require("crypto");
5
5
  /**
6
6
  * 锁管理器,主要用于将不确定的顺序且有冲突的异步操作顺序执行,
7
7
  * 防止异步流程庞大穿插执行造成的数据混乱和错误,常见于请求的处理。
8
+ *
9
+ * ⚠️ 注意:这是一个本地内存锁,仅在当前进程内生效,不支持分布式场景。
10
+ * 如果需要在多进程、多服务器环境下使用,请使用 Redis 等分布式锁方案。
8
11
  */
9
12
  class ServerLockManager {
10
13
  /**
@@ -13,7 +16,7 @@ class ServerLockManager {
13
16
  lockMap = new Map();
14
17
  constructor() {
15
18
  // 定期清理,将过期的信息移除,防止内存泄漏
16
- setTimeout(() => {
19
+ setInterval(() => {
17
20
  const keysToBeDeleted = [];
18
21
  const now = Date.now();
19
22
  for (const entry of this.lockMap.entries()) {
@@ -95,7 +98,7 @@ class ServerLockManager {
95
98
  */
96
99
  sleep() {
97
100
  return new Promise((resolve, reject) => {
98
- setTimeout(resolve, 0);
101
+ setTimeout(resolve, 50);
99
102
  });
100
103
  }
101
104
  }
@@ -13,7 +13,8 @@ function getConfig() {
13
13
  corsAllowOrigin: '*',
14
14
  tlsEnable: false,
15
15
  tlsKey: '',
16
- tlsCert: ''
16
+ tlsCert: '',
17
+ maxBodySize: 10 * 1024 * 1024
17
18
  }, 'SERVER', {
18
19
  port: [(0, validation_1.notNull)(), (0, validation_1.min)(80), (0, validation_1.max)(65535)],
19
20
  timeout: [(0, validation_1.notNull)(), (0, validation_1.min)(1000), (0, validation_1.max)(600000)],
@@ -21,7 +22,8 @@ function getConfig() {
21
22
  corsAllowOrigin: [(0, validation_1.notBlank)()],
22
23
  corsAllowHeaders: [(0, validation_1.notBlank)()],
23
24
  corsAllowMethods: [(0, validation_1.notBlank)()],
24
- tlsEnable: [(0, validation_1.notNull)()]
25
+ tlsEnable: [(0, validation_1.notNull)()],
26
+ maxBodySize: [(0, validation_1.notNull)(), (0, validation_1.min)(0)]
25
27
  });
26
28
  }
27
29
  exports.getConfig = getConfig;
@@ -1,9 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ServerExchange = void 0;
3
+ exports.ServerExchange = exports.PayloadTooLargeError = void 0;
4
+ const config_1 = require("./config");
4
5
  const query_1 = require("./query");
5
6
  const render_1 = require("./render");
6
7
  const text_1 = require("./render/text");
8
+ /**
9
+ * 请求体超出大小限制时抛出的错误
10
+ */
11
+ class PayloadTooLargeError extends Error {
12
+ constructor(message) {
13
+ super(message);
14
+ this.name = 'PayloadTooLargeError';
15
+ }
16
+ }
17
+ exports.PayloadTooLargeError = PayloadTooLargeError;
7
18
  /**
8
19
  * 服务的数据交换对象.
9
20
  */
@@ -15,7 +26,11 @@ class ServerExchange {
15
26
  this.request = request;
16
27
  this.response = response;
17
28
  }
18
- bodyBuffer() {
29
+ /**
30
+ * 读取请求体为 Buffer
31
+ * @param maxSize 最大字节数,不传则使用全局配置 SERVER_MAX_BODY_SIZE,设为 0 不限制
32
+ */
33
+ bodyBuffer(maxSize) {
19
34
  if (this.#bufferPromise) {
20
35
  return this.#bufferPromise;
21
36
  }
@@ -23,21 +38,31 @@ class ServerExchange {
23
38
  if (this.request.readableEnded) {
24
39
  throw new Error('Request has ended!');
25
40
  }
41
+ const limit = maxSize ?? (0, config_1.getConfig)().maxBodySize;
26
42
  let body = [];
43
+ let received = 0;
27
44
  this.request
28
45
  .resume()
29
46
  .on('error', reject)
30
- .on('data', chunk => body.push(chunk))
47
+ .on('data', (chunk) => {
48
+ received += chunk.length;
49
+ if (limit > 0 && received > limit) {
50
+ this.request.pause();
51
+ reject(new PayloadTooLargeError(`Request body exceeds ${limit} bytes`));
52
+ return;
53
+ }
54
+ body.push(chunk);
55
+ })
31
56
  .on('end', () => resolve(Buffer.concat(body)));
32
57
  });
33
58
  return this.#bufferPromise;
34
59
  }
35
- async bodyText() {
36
- const buffer = await this.bodyBuffer();
60
+ async bodyText(maxSize) {
61
+ const buffer = await this.bodyBuffer(maxSize);
37
62
  return buffer.toString('utf-8');
38
63
  }
39
- async bodyJson() {
40
- const buffer = await this.bodyBuffer();
64
+ async bodyJson(maxSize) {
65
+ const buffer = await this.bodyBuffer(maxSize);
41
66
  const bodyText = buffer.toString('utf-8');
42
67
  if (!bodyText || !bodyText.trim()) {
43
68
  return {};
package/dist/mvc/index.js CHANGED
@@ -37,7 +37,7 @@ async function stopWebServer() {
37
37
  if (!SERVER) {
38
38
  return;
39
39
  }
40
- SERVER.stop();
40
+ await SERVER.stop();
41
41
  SERVER = undefined;
42
42
  }
43
43
  exports.stopWebServer = stopWebServer;
@@ -71,8 +71,12 @@ class WokServer {
71
71
  this.handleRequest(req, res).catch(error => {
72
72
  (0, log_1.getLogger)().error(`Handle request failed:${req.url}`, error);
73
73
  if (!res.writableEnded) {
74
- // 响应 500
75
- (0, render_1.renderError)(res, error.message ? error.message : 'Internal Server Error', 500);
74
+ if (error instanceof exchange_1.PayloadTooLargeError) {
75
+ (0, render_1.renderError)(res, error.message, 413);
76
+ }
77
+ else {
78
+ (0, render_1.renderError)(res, error.message ? error.message : 'Internal Server Error', 500);
79
+ }
76
80
  }
77
81
  });
78
82
  });
@@ -90,8 +94,12 @@ class WokServer {
90
94
  this.handleRequest(req, res).catch(error => {
91
95
  (0, log_1.getLogger)().error(`Handle request failed:${req.url}`, error);
92
96
  if (!res.writableEnded) {
93
- // 响应 500
94
- (0, render_1.renderError)(res, error.message ? error.message : 'Internal Server Error', 500);
97
+ if (error instanceof exchange_1.PayloadTooLargeError) {
98
+ (0, render_1.renderError)(res, error.message, 413);
99
+ }
100
+ else {
101
+ (0, render_1.renderError)(res, error.message ? error.message : 'Internal Server Error', 500);
102
+ }
95
103
  }
96
104
  });
97
105
  });
@@ -102,9 +102,6 @@ class BaseMysqlManager {
102
102
  * @returns
103
103
  */
104
104
  async deleteOne(table, criteria) {
105
- if (!Object.keys(criteria).length) {
106
- throw new exception_1.MysqlException('criteria cannot be empty !');
107
- }
108
105
  const res = await this.queryWithConnection(conn => (0, ops_1.deleteMany)(this.opts.config, conn, {
109
106
  table,
110
107
  criteria,
@@ -133,7 +130,7 @@ class BaseMysqlManager {
133
130
  /**
134
131
  * 插入数据. 不支持自增加长id,id必须提前生成,请使用 uuid.
135
132
  * @param table 表信息
136
- * @param data 数据,数据必须是 T 的实例, T 必须是已配置的实体类类型,否则无法完成操作
133
+ * @param data 完整实体数据,必填字段必须存在
137
134
  * @returns 插入后的数据
138
135
  */
139
136
  insert(table, data) {
@@ -142,7 +139,7 @@ class BaseMysqlManager {
142
139
  /**
143
140
  * 批量插入
144
141
  * @param table 表
145
- * @param list 要插入的数据列表
142
+ * @param list 要插入的完整实体数据列表
146
143
  */
147
144
  insertMany(table, list) {
148
145
  return this.queryWithConnection(conn => (0, ops_1.insertMany)(this.opts.config, conn, table, list));
@@ -151,7 +148,7 @@ class BaseMysqlManager {
151
148
  * Upsert 单条数据
152
149
  * 如果主键冲突则更新,否则插入
153
150
  * @param table 表信息
154
- * @param data 数据
151
+ * @param data 完整实体数据
155
152
  * @returns
156
153
  */
157
154
  upsert(table, data) {
@@ -161,7 +158,7 @@ class BaseMysqlManager {
161
158
  * Upsert 多条数据
162
159
  * 如果主键冲突则更新,否则插入
163
160
  * @param table 表
164
- * @param list 要插入的数据列表
161
+ * @param list 要插入的完整实体数据列表
165
162
  * @returns 影响的行数
166
163
  */
167
164
  upsertMany(table, list) {
@@ -171,7 +168,7 @@ class BaseMysqlManager {
171
168
  * Upsert 单条数据(支持自定义更新器)
172
169
  * 如果主键冲突则按自定义逻辑更新,否则插入
173
170
  * @param table 表信息
174
- * @param data 插入的数据
171
+ * @param data 完整实体数据
175
172
  * @param updater 冲突时的更新器
176
173
  * @returns
177
174
  */
@@ -51,7 +51,8 @@ async function deleteMany(config, connection, opts) {
51
51
  }
52
52
  // 数量限制
53
53
  if (opts.limit) {
54
- sql += ` limit ${opts.limit} `;
54
+ sql += ` limit ? `;
55
+ values.push(opts.limit);
55
56
  }
56
57
  const res = await (0, utils_1.promiseQuery)(config, connection, sql, values);
57
58
  return res.affectedRows;
@@ -58,9 +58,11 @@ async function find(config, conn, opts) {
58
58
  }
59
59
  // 数量限制
60
60
  if (opts.limit) {
61
- sql += ` limit ${opts.limit} `;
61
+ sql += ` limit ? `;
62
+ values.push(opts.limit);
62
63
  if (opts.offset) {
63
- sql += ` offset ${opts.offset}`;
64
+ sql += ` offset ?`;
65
+ values.push(opts.offset);
64
66
  }
65
67
  }
66
68
  const res = await (0, utils_1.promiseQuery)(config, conn, sql, values);
@@ -90,9 +92,11 @@ async function findSelect(config, conn, opts) {
90
92
  }
91
93
  // 数量限制
92
94
  if (opts.limit) {
93
- sql += ` limit ${opts.limit} `;
95
+ sql += ` limit ? `;
96
+ values.push(opts.limit);
94
97
  if (opts.offset) {
95
- sql += ` offset ${opts.offset}`;
98
+ sql += ` offset ?`;
99
+ values.push(opts.offset);
96
100
  }
97
101
  }
98
102
  const res = await (0, utils_1.promiseQuery)(config, conn, sql, values);
@@ -1,28 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.insertMany = exports.insert = exports.processInsertValue = void 0;
3
+ exports.insertMany = exports.insert = void 0;
4
4
  const exception_1 = require("../../exception");
5
5
  const utils_1 = require("../utils");
6
6
  const utils_2 = require("./utils");
7
- /**
8
- * 处理 insert value,支持表达式
9
- * @returns { frag: SQL 片段, values: 参数值数组 }
10
- */
11
- function processInsertValue(value) {
12
- if (Array.isArray(value)) {
13
- if (value[0] === 'now') {
14
- return { frag: 'NOW()', values: [] };
15
- }
16
- if (value[0] === 'set') {
17
- return { frag: '?', values: [(0, utils_2.processColumnValue)(value[1])] };
18
- }
19
- if (value[0] === 'expr') {
20
- return { frag: value[1], values: value[2] || [] };
21
- }
22
- }
23
- return { frag: '?', values: [(0, utils_2.processColumnValue)(value)] };
24
- }
25
- exports.processInsertValue = processInsertValue;
26
7
  /**
27
8
  * 为表插入数据
28
9
  * @param connection
@@ -49,23 +30,22 @@ async function insert(config, connection, table, data) {
49
30
  columnsSet.add(table.updatedDate.column);
50
31
  }
51
32
  const columns = Array.from(columnsSet);
52
- // 构建 sql,逐列处理以支持表达式
53
- const fragList = [];
54
- const insertValues = [];
55
- for (const col of columns) {
56
- const { frag, values: vs } = processInsertValue(data[col]);
57
- fragList.push(frag);
58
- insertValues.push(...vs);
59
- }
60
- const sql = `insert into ??(${columns.map(() => '??').join(',')}) values(${fragList.join(',')})`;
61
- const values = [table.tableName, ...columns, ...insertValues];
33
+ // 构建 sql
34
+ const sql = `insert into ??(${columns.map(() => '??').join(',')}) values(${columns.map(() => '?').join(',')})`;
35
+ const values = [
36
+ table.tableName,
37
+ ...columns,
38
+ ...columns.map(col => (0, utils_2.processColumnValue)(data[col]))
39
+ ];
62
40
  const res = await (0, utils_1.promiseQuery)(config, connection, sql, values);
63
41
  const packet = res;
64
42
  if (packet.affectedRows !== 1) {
65
43
  throw new exception_1.MysqlException(`Insert failed,table:${table.tableName},primary key: ${data[table.id]}`);
66
44
  }
67
45
  // 自动生成的id处理
68
- if (packet.insertId && (data[table.id] === undefined || data[table.id] === null)) {
46
+ if (packet.insertId &&
47
+ (data[table.id] === undefined || data[table.id] === null)) {
48
+ ;
69
49
  data[table.id] = packet.insertId;
70
50
  }
71
51
  return data;
@@ -106,21 +86,16 @@ async function insertMany(config, connection, table, list) {
106
86
  if (idx > 0) {
107
87
  sql += ',';
108
88
  }
109
- const fragList = [];
110
- const rowValues = [];
111
89
  if (table.createdDate) {
90
+ ;
112
91
  data[table.createdDate.column] = createdData;
113
92
  }
114
93
  if (table.updatedDate) {
94
+ ;
115
95
  data[table.updatedDate.column] = updatedDate;
116
96
  }
117
- for (const col of columns) {
118
- const { frag, values: vs } = processInsertValue(data[col]);
119
- fragList.push(frag);
120
- rowValues.push(...vs);
121
- }
122
- sql += `(${fragList.join(',')})`;
123
- values.push(...rowValues);
97
+ sql += `(${columns.map(() => '?').join(',')})`;
98
+ values.push(...columns.map(col => (0, utils_2.processColumnValue)(data[col])));
124
99
  });
125
100
  const res = await (0, utils_1.promiseQuery)(config, connection, sql, values);
126
101
  const rsh = res;
@@ -213,7 +213,8 @@ async function updateMany(config, connection, opts) {
213
213
  }
214
214
  // 数量限制
215
215
  if (opts.limit) {
216
- sql += ` limit ${opts.limit} `;
216
+ sql += ` limit ? `;
217
+ values.push(opts.limit);
217
218
  }
218
219
  const res = await (0, utils_1.promiseQuery)(config, connection, sql, values);
219
220
  const packet = res;
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.upsertWithUpdater = exports.upsertMany = exports.upsert = void 0;
4
4
  const utils_1 = require("../utils");
5
- const insert_1 = require("./insert");
5
+ const utils_2 = require("./utils");
6
6
  const update_1 = require("./update");
7
7
  /**
8
8
  * Upsert 单条数据
@@ -33,26 +33,19 @@ async function upsert(config, connection, table, data) {
33
33
  }
34
34
  const columns = Array.from(columnsSet);
35
35
  // 构建 insert values
36
- const fragList = [];
37
- const insertValues = [];
38
- for (const col of columns) {
39
- const { frag, values: vs } = (0, insert_1.processInsertValue)(data[col]);
40
- fragList.push(frag);
41
- insertValues.push(...vs);
42
- }
43
- const insertSql = `insert into ??(${columns.map(() => '??').join(',')}) values(${fragList.join(',')})`;
36
+ const insertSql = `insert into ??(${columns.map(() => '??').join(',')}) values(${columns.map(() => '?').join(',')})`;
37
+ const insertValues = [table.tableName, ...columns, ...columns.map(col => (0, utils_2.processColumnValue)(data[col]))];
44
38
  // 构建 on duplicate key update(排除 id)
45
39
  const updateColumns = columns.filter(col => col !== table.id);
46
40
  const updateFragments = [];
47
41
  const updateValues = [];
48
42
  for (const col of updateColumns) {
49
- const { frag, values: vs } = (0, insert_1.processInsertValue)(data[col]);
50
- updateFragments.push(`?? = ${frag}`);
51
- updateValues.push(col, ...vs);
43
+ updateFragments.push(`?? = ?`);
44
+ updateValues.push(col, (0, utils_2.processColumnValue)(data[col]));
52
45
  }
53
46
  const updateSql = ` on duplicate key update ${updateFragments.join(',')}`;
54
47
  const sql = insertSql + updateSql;
55
- const values = [table.tableName, ...columns, ...insertValues, ...updateValues];
48
+ const values = [...insertValues, ...updateValues];
56
49
  const res = await (0, utils_1.promiseQuery)(config, connection, sql, values);
57
50
  const packet = res;
58
51
  if (packet.insertId && (data[table.id] === undefined || data[table.id] === null)) {
@@ -98,21 +91,14 @@ async function upsertMany(config, connection, table, list) {
98
91
  if (idx > 0) {
99
92
  sql += ',';
100
93
  }
101
- const fragList = [];
102
- const rowValues = [];
103
94
  if (table.createdDate) {
104
95
  data[table.createdDate.column] = createdData;
105
96
  }
106
97
  if (table.updatedDate) {
107
98
  data[table.updatedDate.column] = updatedDate;
108
99
  }
109
- for (const col of columns) {
110
- const { frag, values: vs } = (0, insert_1.processInsertValue)(data[col]);
111
- fragList.push(frag);
112
- rowValues.push(...vs);
113
- }
114
- sql += `(${fragList.join(',')})`;
115
- values.push(...rowValues);
100
+ sql += `(${columns.map(() => '?').join(',')})`;
101
+ values.push(...columns.map(col => (0, utils_2.processColumnValue)(data[col])));
116
102
  });
117
103
  const updateColumns = columns.filter(col => col !== table.id);
118
104
  sql += ` on duplicate key update ${updateColumns.map(() => '?? = values(??)').join(',')}`;
@@ -156,18 +142,12 @@ async function upsertWithUpdater(config, connection, table, data, updater) {
156
142
  }
157
143
  const columns = Array.from(columnsSet);
158
144
  // 构建 insert values
159
- const fragList = [];
160
- const insertValues = [];
161
- for (const col of columns) {
162
- const { frag, values: vs } = (0, insert_1.processInsertValue)(data[col]);
163
- fragList.push(frag);
164
- insertValues.push(...vs);
165
- }
166
- const insertSql = `insert into ??(${columns.map(() => '??').join(',')}) values(${fragList.join(',')})`;
145
+ const insertSql = `insert into ??(${columns.map(() => '??').join(',')}) values(${columns.map(() => '?').join(',')})`;
146
+ const insertValues = [table.tableName, ...columns, ...columns.map(col => (0, utils_2.processColumnValue)(data[col]))];
167
147
  const convertRes = (0, update_1.updatorToSql)(table, updater);
168
148
  const updateSql = ` on duplicate key update ${convertRes.sql}`;
169
149
  const sql = insertSql + updateSql;
170
- const values = [table.tableName, ...columns, ...insertValues, ...convertRes.values];
150
+ const values = [...insertValues, ...convertRes.values];
171
151
  const res = await (0, utils_1.promiseQuery)(config, connection, sql, values);
172
152
  const packet = res;
173
153
  if (packet.insertId && (data[table.id] === undefined || data[table.id] === null)) {
@@ -30,14 +30,21 @@ async function migrate(config, conn) {
30
30
  if (!file.endsWith('.sql')) {
31
31
  throw new Error(`版本文件名没有以 .sql 为后缀:${file}`);
32
32
  }
33
- const version = parseInt(file.substring(0, file.length - 4));
34
- if (isNaN(version)) {
35
- throw new Error(`Version file is not named with a number:${file}`);
33
+ const match = file.match(/^(\d+)/);
34
+ if (!match) {
35
+ throw new Error(`Version file name must start with a number:${file}`);
36
36
  }
37
+ const version = parseInt(match[1]);
37
38
  versions.push({ version, filePath });
38
39
  }
39
40
  // 排序,判定顺序
40
41
  versions.sort((o1, o2) => o1.version - o2.version);
42
+ // 去重校验
43
+ for (let i = 1; i < versions.length; i++) {
44
+ if (versions[i].version === versions[i - 1].version) {
45
+ throw new Error(`Duplicate version number ${versions[i].version} in files: ${versions[i - 1].filePath} and ${versions[i].filePath}`);
46
+ }
47
+ }
41
48
  for (let i = 0; i < versions.length; i++) {
42
49
  const version = versions[i];
43
50
  if (version.version !== i + 1) {