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
@@ -1,8 +1,19 @@
1
1
  import { IncomingMessage, ServerResponse } from 'http'
2
+ import { getConfig } from './config'
2
3
  import { QueryString } from './query'
3
4
  import { HtmlStuct, renderError, renderFile, renderHtml, renderJson } from './render'
4
5
  import { renderText } from './render/text'
5
6
 
7
+ /**
8
+ * 请求体超出大小限制时抛出的错误
9
+ */
10
+ export class PayloadTooLargeError extends Error {
11
+ constructor(message: string) {
12
+ super(message)
13
+ this.name = 'PayloadTooLargeError'
14
+ }
15
+ }
16
+
6
17
  /**
7
18
  * 服务的数据交换对象.
8
19
  */
@@ -11,7 +22,11 @@ export class ServerExchange {
11
22
 
12
23
  constructor(readonly request: IncomingMessage, readonly response: ServerResponse) {}
13
24
 
14
- bodyBuffer(): Promise<Buffer> {
25
+ /**
26
+ * 读取请求体为 Buffer
27
+ * @param maxSize 最大字节数,不传则使用全局配置 SERVER_MAX_BODY_SIZE,设为 0 不限制
28
+ */
29
+ bodyBuffer(maxSize?: number): Promise<Buffer> {
15
30
  if (this.#bufferPromise) {
16
31
  return this.#bufferPromise
17
32
  }
@@ -19,23 +34,33 @@ export class ServerExchange {
19
34
  if (this.request.readableEnded) {
20
35
  throw new Error('Request has ended!')
21
36
  }
37
+ const limit = maxSize ?? getConfig().maxBodySize
22
38
  let body: Buffer[] = []
39
+ let received = 0
23
40
  this.request
24
41
  .resume()
25
42
  .on('error', reject)
26
- .on('data', chunk => body.push(chunk))
43
+ .on('data', (chunk: Buffer) => {
44
+ received += chunk.length
45
+ if (limit > 0 && received > limit) {
46
+ this.request.pause()
47
+ reject(new PayloadTooLargeError(`Request body exceeds ${limit} bytes`))
48
+ return
49
+ }
50
+ body.push(chunk)
51
+ })
27
52
  .on('end', () => resolve(Buffer.concat(body)))
28
53
  })
29
54
  return this.#bufferPromise
30
55
  }
31
56
 
32
- async bodyText(): Promise<string> {
33
- const buffer = await this.bodyBuffer()
57
+ async bodyText(maxSize?: number): Promise<string> {
58
+ const buffer = await this.bodyBuffer(maxSize)
34
59
  return buffer.toString('utf-8')
35
60
  }
36
61
 
37
- async bodyJson<T>(): Promise<T> {
38
- const buffer = await this.bodyBuffer()
62
+ async bodyJson<T>(maxSize?: number): Promise<T> {
63
+ const buffer = await this.bodyBuffer(maxSize)
39
64
  const bodyText = buffer.toString('utf-8')
40
65
  if (!bodyText || !bodyText.trim()) {
41
66
  return {} as any
package/src/mvc/index.ts CHANGED
@@ -34,7 +34,7 @@ export async function stopWebServer() {
34
34
  if (!SERVER) {
35
35
  return
36
36
  }
37
- SERVER.stop()
37
+ await SERVER.stop()
38
38
  SERVER = undefined
39
39
  }
40
40
 
package/src/mvc/server.ts CHANGED
@@ -7,7 +7,7 @@ import { isAbsolute, resolve } from 'path'
7
7
  import { getLogger } from '../log'
8
8
  import { accessLogInterceptor } from './access-log'
9
9
  import { WebConfig, getConfig } from './config'
10
- import { ServerExchange } from './exchange'
10
+ import { PayloadTooLargeError, ServerExchange } from './exchange'
11
11
  import { Interceptor } from './interceptor'
12
12
  import { renderError } from './render'
13
13
  import { RouterHandler, Routers } from './router'
@@ -103,8 +103,11 @@ export class WokServer {
103
103
  this.handleRequest(req, res).catch(error => {
104
104
  getLogger().error(`Handle request failed:${req.url}`, error)
105
105
  if (!res.writableEnded) {
106
- // 响应 500
107
- renderError(res, error.message ? error.message : 'Internal Server Error', 500)
106
+ if (error instanceof PayloadTooLargeError) {
107
+ renderError(res, error.message, 413)
108
+ } else {
109
+ renderError(res, error.message ? error.message : 'Internal Server Error', 500)
110
+ }
108
111
  }
109
112
  })
110
113
  })
@@ -123,8 +126,11 @@ export class WokServer {
123
126
  this.handleRequest(req, res).catch(error => {
124
127
  getLogger().error(`Handle request failed:${req.url}`, error)
125
128
  if (!res.writableEnded) {
126
- // 响应 500
127
- renderError(res, error.message ? error.message : 'Internal Server Error', 500)
129
+ if (error instanceof PayloadTooLargeError) {
130
+ renderError(res, error.message, 413)
131
+ } else {
132
+ renderError(res, error.message ? error.message : 'Internal Server Error', 500)
133
+ }
128
134
  }
129
135
  })
130
136
  }
@@ -12,7 +12,6 @@ import {
12
12
  OrderBy,
13
13
  UpdateOpts,
14
14
  Updater,
15
- InsertValue,
16
15
  count,
17
16
  deleteMany,
18
17
  deleteById,
@@ -138,10 +137,7 @@ export abstract class BaseMysqlManager {
138
137
  * @param criteria
139
138
  * @returns
140
139
  */
141
- async deleteOne<T>(table: Table<T>, criteria: Partial<T>) {
142
- if (!Object.keys(criteria).length) {
143
- throw new MysqlException('criteria cannot be empty !')
144
- }
140
+ async deleteOne<T>(table: Table<T>, criteria: MixCriteria<T>) {
145
141
  const res = await this.queryWithConnection(conn =>
146
142
  deleteMany(this.opts.config, conn, {
147
143
  table,
@@ -179,50 +175,52 @@ export abstract class BaseMysqlManager {
179
175
  /**
180
176
  * 插入数据. 不支持自增加长id,id必须提前生成,请使用 uuid.
181
177
  * @param table 表信息
182
- * @param data 数据,数据必须是 T 的实例, T 必须是已配置的实体类类型,否则无法完成操作
178
+ * @param data 完整实体数据,必填字段必须存在
183
179
  * @returns 插入后的数据
184
180
  */
185
- insert<T>(table: Table<T>, data: InsertValue<T>): Promise<T> {
181
+ insert<T>(table: Table<T>, data: T): Promise<T> {
186
182
  return this.queryWithConnection(conn => insert(this.opts.config, conn, table, data))
187
183
  }
188
184
  /**
189
185
  * 批量插入
190
186
  * @param table 表
191
- * @param list 要插入的数据列表
187
+ * @param list 要插入的完整实体数据列表
192
188
  */
193
- insertMany<T>(table: Table<T>, list: InsertValue<T>[]): Promise<void> {
189
+ insertMany<T>(table: Table<T>, list: T[]): Promise<void> {
194
190
  return this.queryWithConnection(conn => insertMany(this.opts.config, conn, table, list))
195
191
  }
196
192
  /**
197
193
  * Upsert 单条数据
198
194
  * 如果主键冲突则更新,否则插入
199
195
  * @param table 表信息
200
- * @param data 数据
196
+ * @param data 完整实体数据
201
197
  * @returns
202
198
  */
203
- upsert<T>(table: Table<T>, data: InsertValue<T>): Promise<T> {
199
+ upsert<T>(table: Table<T>, data: T): Promise<T> {
204
200
  return this.queryWithConnection(conn => upsert(this.opts.config, conn, table, data))
205
201
  }
206
202
  /**
207
203
  * Upsert 多条数据
208
204
  * 如果主键冲突则更新,否则插入
209
205
  * @param table 表
210
- * @param list 要插入的数据列表
206
+ * @param list 要插入的完整实体数据列表
211
207
  * @returns 影响的行数
212
208
  */
213
- upsertMany<T>(table: Table<T>, list: InsertValue<T>[]): Promise<number> {
209
+ upsertMany<T>(table: Table<T>, list: T[]): Promise<number> {
214
210
  return this.queryWithConnection(conn => upsertMany(this.opts.config, conn, table, list))
215
211
  }
216
212
  /**
217
213
  * Upsert 单条数据(支持自定义更新器)
218
214
  * 如果主键冲突则按自定义逻辑更新,否则插入
219
215
  * @param table 表信息
220
- * @param data 插入的数据
216
+ * @param data 完整实体数据
221
217
  * @param updater 冲突时的更新器
222
218
  * @returns
223
219
  */
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))
220
+ upsertWithUpdater<T>(table: Table<T>, data: T, updater: Updater<T>): Promise<T> {
221
+ return this.queryWithConnection(conn =>
222
+ upsertWithUpdater(this.opts.config, conn, table, data, updater)
223
+ )
226
224
  }
227
225
  /**
228
226
  * 更新
@@ -307,7 +305,9 @@ export abstract class BaseMysqlManager {
307
305
  * @param opts
308
306
  * @returns
309
307
  */
310
- paginateSelect<T, K extends keyof T>(opts: MysqlPaginateSelectOpts<T, K>): Promise<MysqlPage<Pick<T, K>>> {
308
+ paginateSelect<T, K extends keyof T>(
309
+ opts: MysqlPaginateSelectOpts<T, K>
310
+ ): Promise<MysqlPage<Pick<T, K>>> {
311
311
  return this.queryWithConnection(conn => paginateSelect(this.opts.config, conn, opts))
312
312
  }
313
313
 
@@ -84,7 +84,8 @@ export async function deleteMany<T>(
84
84
  }
85
85
  // 数量限制
86
86
  if (opts.limit) {
87
- sql += ` limit ${opts.limit} `
87
+ sql += ` limit ? `
88
+ values.push(opts.limit)
88
89
  }
89
90
  const res = await promiseQuery(config, connection, sql, values)
90
91
  return (res as ResultSetHeader).affectedRows
@@ -96,9 +96,11 @@ export async function find<T>(
96
96
  }
97
97
  // 数量限制
98
98
  if (opts.limit) {
99
- sql += ` limit ${opts.limit} `
99
+ sql += ` limit ? `
100
+ values.push(opts.limit)
100
101
  if (opts.offset) {
101
- sql += ` offset ${opts.offset}`
102
+ sql += ` offset ?`
103
+ values.push(opts.offset)
102
104
  }
103
105
  }
104
106
  const res = await promiseQuery(config, conn, sql, values)
@@ -142,9 +144,11 @@ export async function findSelect<T, K extends keyof T>(
142
144
  }
143
145
  // 数量限制
144
146
  if (opts.limit) {
145
- sql += ` limit ${opts.limit} `
147
+ sql += ` limit ? `
148
+ values.push(opts.limit)
146
149
  if (opts.offset) {
147
- sql += ` offset ${opts.offset}`
150
+ sql += ` offset ?`
151
+ values.push(opts.offset)
148
152
  }
149
153
  }
150
154
  const res = await promiseQuery(config, conn, sql, values)
@@ -5,37 +5,6 @@ import { promiseQuery } from '../utils'
5
5
  import { MysqlConfig } from '../../config'
6
6
  import { processColumnValue } from './utils'
7
7
 
8
- /**
9
- * 插入值类型,支持在 INSERT VALUES 中使用表达式
10
- */
11
- export type InsertValue<T> = {
12
- [K in keyof T]?:
13
- | T[K]
14
- | ['now']
15
- | ['set', T[K]]
16
- | ['expr', string]
17
- | ['expr', string, any[]]
18
- }
19
-
20
- /**
21
- * 处理 insert value,支持表达式
22
- * @returns { frag: SQL 片段, values: 参数值数组 }
23
- */
24
- export function processInsertValue(value: any): { frag: string; values: any[] } {
25
- if (Array.isArray(value)) {
26
- if (value[0] === 'now') {
27
- return { frag: 'NOW()', values: [] }
28
- }
29
- if (value[0] === 'set') {
30
- return { frag: '?', values: [processColumnValue(value[1])] }
31
- }
32
- if (value[0] === 'expr') {
33
- return { frag: value[1], values: value[2] || [] }
34
- }
35
- }
36
- return { frag: '?', values: [processColumnValue(value)] }
37
- }
38
-
39
8
  /**
40
9
  * 为表插入数据
41
10
  * @param connection
@@ -47,7 +16,7 @@ export async function insert<T>(
47
16
  config: MysqlConfig,
48
17
  connection: PoolConnection,
49
18
  table: Table<T>,
50
- data: InsertValue<T>
19
+ data: T
51
20
  ): Promise<T> {
52
21
  // 列信息,使用 set 防止 columns 中重复配置 id 和更新创建时间列
53
22
  let columnsSet: Set<keyof T> = new Set()
@@ -58,37 +27,37 @@ export async function insert<T>(
58
27
  table.columns.forEach(col => columnsSet.add(col))
59
28
  if (table.createdDate) {
60
29
  const createdData = table.createdDate.type === 'date' ? new Date() : new Date().getTime()
61
- data[table.createdDate.column] = createdData as any
30
+ ;(data as any)[table.createdDate.column] = createdData
62
31
  columnsSet.add(table.createdDate.column)
63
32
  }
64
33
  if (table.updatedDate) {
65
34
  const updatedDate = table.updatedDate.type === 'date' ? new Date() : new Date().getTime()
66
- data[table.updatedDate.column] = updatedDate as any
35
+ ;(data as any)[table.updatedDate.column] = updatedDate
67
36
  columnsSet.add(table.updatedDate.column)
68
37
  }
69
38
  const columns = Array.from(columnsSet)
70
- // 构建 sql,逐列处理以支持表达式
71
- const fragList: string[] = []
72
- const insertValues: any[] = []
73
- for (const col of columns) {
74
- const { frag, values: vs } = processInsertValue(data[col])
75
- fragList.push(frag)
76
- insertValues.push(...vs)
77
- }
78
- const sql = `insert into ??(${columns.map(() => '??').join(',')}) values(${fragList.join(',')})`
79
- const values: any[] = [table.tableName, ...columns, ...insertValues]
39
+ // 构建 sql
40
+ const sql = `insert into ??(${columns.map(() => '??').join(',')}) values(${columns.map(() => '?').join(',')})`
41
+ const values: any[] = [
42
+ table.tableName,
43
+ ...columns,
44
+ ...columns.map(col => processColumnValue((data as any)[col]))
45
+ ]
80
46
  const res = await promiseQuery(config, connection, sql, values)
81
47
  const packet = res as ResultSetHeader
82
48
  if (packet.affectedRows !== 1) {
83
49
  throw new MysqlException(
84
- `Insert failed,table:${table.tableName},primary key: ${data[table.id]}`
50
+ `Insert failed,table:${table.tableName},primary key: ${(data as any)[table.id]}`
85
51
  )
86
52
  }
87
53
  // 自动生成的id处理
88
- if (packet.insertId && (data[table.id] === undefined || data[table.id] === null)) {
89
- data[table.id] = packet.insertId as any
54
+ if (
55
+ packet.insertId &&
56
+ ((data as any)[table.id] === undefined || (data as any)[table.id] === null)
57
+ ) {
58
+ ;(data as any)[table.id] = packet.insertId
90
59
  }
91
- return data as unknown as T
60
+ return data
92
61
  }
93
62
  /**
94
63
  * 一次插入多条记录
@@ -100,7 +69,7 @@ export async function insertMany<T>(
100
69
  config: MysqlConfig,
101
70
  connection: PoolConnection,
102
71
  table: Table<T>,
103
- list: InsertValue<T>[]
72
+ list: T[]
104
73
  ): Promise<void> {
105
74
  if (!list.length) {
106
75
  return
@@ -108,7 +77,7 @@ export async function insertMany<T>(
108
77
  // 列信息,使用 set 防止 columns 中重复配置 id 和更新创建时间列
109
78
  let columnsSet: Set<keyof T> = new Set()
110
79
  // 批量插入必须统一使用 id 或不使用 id,以第一条记录为准
111
- if (list[0][table.id]) {
80
+ if ((list[0] as any)[table.id]) {
112
81
  columnsSet.add(table.id)
113
82
  }
114
83
  table.columns.forEach(col => columnsSet.add(col))
@@ -130,21 +99,14 @@ export async function insertMany<T>(
130
99
  if (idx > 0) {
131
100
  sql += ','
132
101
  }
133
- const fragList: string[] = []
134
- const rowValues: any[] = []
135
102
  if (table.createdDate) {
136
- data[table.createdDate.column] = createdData as any
103
+ ;(data as any)[table.createdDate.column] = createdData
137
104
  }
138
105
  if (table.updatedDate) {
139
- data[table.updatedDate.column] = updatedDate as any
140
- }
141
- for (const col of columns) {
142
- const { frag, values: vs } = processInsertValue(data[col])
143
- fragList.push(frag)
144
- rowValues.push(...vs)
106
+ ;(data as any)[table.updatedDate.column] = updatedDate
145
107
  }
146
- sql += `(${fragList.join(',')})`
147
- values.push(...rowValues)
108
+ sql += `(${columns.map(() => '?').join(',')})`
109
+ values.push(...columns.map(col => processColumnValue((data as any)[col])))
148
110
  })
149
111
 
150
112
  const res = await promiseQuery(config, connection, sql, values)
@@ -310,7 +310,8 @@ export async function updateMany<T>(
310
310
  }
311
311
  // 数量限制
312
312
  if (opts.limit) {
313
- sql += ` limit ${opts.limit} `
313
+ sql += ` limit ? `
314
+ values.push(opts.limit)
314
315
  }
315
316
  const res = await promiseQuery(config, connection, sql, values)
316
317
  const packet = res as ResultSetHeader
@@ -2,7 +2,7 @@ import { PoolConnection, ResultSetHeader } from 'mysql2'
2
2
  import { MysqlConfig } from '../../config'
3
3
  import { Table } from '../../table-info'
4
4
  import { promiseQuery } from '../utils'
5
- import { InsertValue, processInsertValue } from './insert'
5
+ import { processColumnValue } from './utils'
6
6
  import { Updater, updatorToSql } from './update'
7
7
 
8
8
  /**
@@ -18,10 +18,10 @@ export async function upsert<T>(
18
18
  config: MysqlConfig,
19
19
  connection: PoolConnection,
20
20
  table: Table<T>,
21
- data: InsertValue<T>
21
+ data: T
22
22
  ): Promise<T> {
23
23
  let columnsSet: Set<keyof T> = new Set()
24
- if (data[table.id]) {
24
+ if ((data as any)[table.id]) {
25
25
  columnsSet.add(table.id)
26
26
  }
27
27
  table.columns.forEach(col => columnsSet.add(col))
@@ -31,50 +31,43 @@ export async function upsert<T>(
31
31
 
32
32
  if (table.createdDate) {
33
33
  const createdData = table.createdDate.type === 'date' ? now : nowTimestamp
34
- data[table.createdDate.column] = createdData as any
34
+ ;(data as any)[table.createdDate.column] = createdData
35
35
  columnsSet.add(table.createdDate.column)
36
36
  }
37
37
  if (table.updatedDate) {
38
38
  const updatedDate = table.updatedDate.type === 'date' ? now : nowTimestamp
39
- data[table.updatedDate.column] = updatedDate as any
39
+ ;(data as any)[table.updatedDate.column] = updatedDate
40
40
  columnsSet.add(table.updatedDate.column)
41
41
  }
42
42
 
43
43
  const columns = Array.from(columnsSet)
44
44
 
45
45
  // 构建 insert values
46
- const fragList: string[] = []
47
- const insertValues: any[] = []
48
- for (const col of columns) {
49
- const { frag, values: vs } = processInsertValue(data[col])
50
- fragList.push(frag)
51
- insertValues.push(...vs)
52
- }
53
- const insertSql = `insert into ??(${columns.map(() => '??').join(',')}) values(${fragList.join(',')})`
46
+ const insertSql = `insert into ??(${columns.map(() => '??').join(',')}) values(${columns.map(() => '?').join(',')})`
47
+ const insertValues: any[] = [table.tableName, ...columns, ...columns.map(col => processColumnValue((data as any)[col]))]
54
48
 
55
49
  // 构建 on duplicate key update(排除 id)
56
50
  const updateColumns = columns.filter(col => col !== table.id)
57
51
  const updateFragments: string[] = []
58
52
  const updateValues: any[] = []
59
53
  for (const col of updateColumns) {
60
- const { frag, values: vs } = processInsertValue(data[col])
61
- updateFragments.push(`?? = ${frag}`)
62
- updateValues.push(col, ...vs)
54
+ updateFragments.push(`?? = ?`)
55
+ updateValues.push(col, processColumnValue((data as any)[col]))
63
56
  }
64
57
  const updateSql = ` on duplicate key update ${updateFragments.join(',')}`
65
58
 
66
59
  const sql = insertSql + updateSql
67
60
 
68
- const values = [table.tableName, ...columns, ...insertValues, ...updateValues]
61
+ const values = [...insertValues, ...updateValues]
69
62
 
70
63
  const res = await promiseQuery(config, connection, sql, values)
71
64
  const packet = res as ResultSetHeader
72
65
 
73
- if (packet.insertId && (data[table.id] === undefined || data[table.id] === null)) {
74
- data[table.id] = packet.insertId as any
66
+ if (packet.insertId && ((data as any)[table.id] === undefined || (data as any)[table.id] === null)) {
67
+ (data as any)[table.id] = packet.insertId
75
68
  }
76
69
 
77
- return data as unknown as T
70
+ return data
78
71
  }
79
72
 
80
73
  /**
@@ -90,14 +83,14 @@ export async function upsertMany<T>(
90
83
  config: MysqlConfig,
91
84
  connection: PoolConnection,
92
85
  table: Table<T>,
93
- list: InsertValue<T>[]
86
+ list: T[]
94
87
  ): Promise<number> {
95
88
  if (!list.length) {
96
89
  return 0
97
90
  }
98
91
 
99
92
  let columnsSet: Set<keyof T> = new Set()
100
- if (list[0][table.id]) {
93
+ if ((list[0] as any)[table.id]) {
101
94
  columnsSet.add(table.id)
102
95
  }
103
96
  table.columns.forEach(col => columnsSet.add(col))
@@ -125,21 +118,14 @@ export async function upsertMany<T>(
125
118
  if (idx > 0) {
126
119
  sql += ','
127
120
  }
128
- const fragList: string[] = []
129
- const rowValues: any[] = []
130
121
  if (table.createdDate) {
131
- data[table.createdDate.column] = createdData as any
122
+ (data as any)[table.createdDate.column] = createdData
132
123
  }
133
124
  if (table.updatedDate) {
134
- data[table.updatedDate.column] = updatedDate as any
135
- }
136
- for (const col of columns) {
137
- const { frag, values: vs } = processInsertValue(data[col])
138
- fragList.push(frag)
139
- rowValues.push(...vs)
125
+ (data as any)[table.updatedDate.column] = updatedDate
140
126
  }
141
- sql += `(${fragList.join(',')})`
142
- values.push(...rowValues)
127
+ sql += `(${columns.map(() => '?').join(',')})`
128
+ values.push(...columns.map(col => processColumnValue((data as any)[col])))
143
129
  })
144
130
 
145
131
  const updateColumns = columns.filter(col => col !== table.id)
@@ -166,15 +152,15 @@ export async function upsertWithUpdater<T>(
166
152
  config: MysqlConfig,
167
153
  connection: PoolConnection,
168
154
  table: Table<T>,
169
- data: InsertValue<T>,
155
+ data: T,
170
156
  updater: Updater<T>
171
157
  ): Promise<T> {
172
158
  let columnsSet: Set<keyof T> = new Set()
173
- if (data[table.id]) {
159
+ if ((data as any)[table.id]) {
174
160
  columnsSet.add(table.id)
175
161
  }
176
162
  table.columns.forEach(col => {
177
- if (data[col] !== undefined) {
163
+ if ((data as any)[col] !== undefined) {
178
164
  columnsSet.add(col)
179
165
  }
180
166
  })
@@ -184,26 +170,20 @@ export async function upsertWithUpdater<T>(
184
170
 
185
171
  if (table.createdDate) {
186
172
  const createdData = table.createdDate.type === 'date' ? now : nowTimestamp
187
- data[table.createdDate.column] = createdData as any
173
+ ;(data as any)[table.createdDate.column] = createdData
188
174
  columnsSet.add(table.createdDate.column)
189
175
  }
190
176
  if (table.updatedDate) {
191
177
  const updatedDate = table.updatedDate.type === 'date' ? now : nowTimestamp
192
- data[table.updatedDate.column] = updatedDate as any
178
+ ;(data as any)[table.updatedDate.column] = updatedDate
193
179
  columnsSet.add(table.updatedDate.column)
194
180
  }
195
181
 
196
182
  const columns = Array.from(columnsSet)
197
183
 
198
184
  // 构建 insert values
199
- const fragList: string[] = []
200
- const insertValues: any[] = []
201
- for (const col of columns) {
202
- const { frag, values: vs } = processInsertValue(data[col])
203
- fragList.push(frag)
204
- insertValues.push(...vs)
205
- }
206
- const insertSql = `insert into ??(${columns.map(() => '??').join(',')}) values(${fragList.join(',')})`
185
+ const insertSql = `insert into ??(${columns.map(() => '??').join(',')}) values(${columns.map(() => '?').join(',')})`
186
+ const insertValues: any[] = [table.tableName, ...columns, ...columns.map(col => processColumnValue((data as any)[col]))]
207
187
 
208
188
  const convertRes = updatorToSql(table, updater)
209
189
 
@@ -211,14 +191,14 @@ export async function upsertWithUpdater<T>(
211
191
 
212
192
  const sql = insertSql + updateSql
213
193
 
214
- const values = [table.tableName, ...columns, ...insertValues, ...convertRes.values]
194
+ const values = [...insertValues, ...convertRes.values]
215
195
 
216
196
  const res = await promiseQuery(config, connection, sql, values)
217
197
  const packet = res as ResultSetHeader
218
198
 
219
- if (packet.insertId && (data[table.id] === undefined || data[table.id] === null)) {
220
- data[table.id] = packet.insertId as any
199
+ if (packet.insertId && ((data as any)[table.id] === undefined || (data as any)[table.id] === null)) {
200
+ (data as any)[table.id] = packet.insertId
221
201
  }
222
202
 
223
- return data as unknown as T
224
- }
203
+ return data
204
+ }
@@ -38,14 +38,25 @@ export async function migrate(config: MysqlConfig, conn: Connection) {
38
38
  if (!file.endsWith('.sql')) {
39
39
  throw new Error(`版本文件名没有以 .sql 为后缀:${file}`)
40
40
  }
41
- const version = parseInt(file.substring(0, file.length - 4))
42
- if (isNaN(version)) {
43
- throw new Error(`Version file is not named with a number:${file}`)
41
+ const match = file.match(/^(\d+)/)
42
+ if (!match) {
43
+ throw new Error(`Version file name must start with a number:${file}`)
44
44
  }
45
+ const version = parseInt(match[1])
45
46
  versions.push({ version, filePath })
46
47
  }
47
48
  // 排序,判定顺序
48
49
  versions.sort((o1, o2) => o1.version - o2.version)
50
+ // 去重校验
51
+ for (let i = 1; i < versions.length; i++) {
52
+ if (versions[i].version === versions[i - 1].version) {
53
+ throw new Error(
54
+ `Duplicate version number ${versions[i].version} in files: ${
55
+ versions[i - 1].filePath
56
+ } and ${versions[i].filePath}`
57
+ )
58
+ }
59
+ }
49
60
  for (let i = 0; i < versions.length; i++) {
50
61
  const version = versions[i]
51
62
  if (version.version !== i + 1) {
@@ -1,7 +1,7 @@
1
1
  /// <reference types="node" />
2
2
  /// <reference types="node" />
3
3
  /// <reference types="node" />
4
- import { IncomingHttpHeaders, Agent as HttpAgent } from 'http';
4
+ import { Agent as HttpAgent, IncomingHttpHeaders } from 'http';
5
5
  import { Agent as HttpsAgent } from 'https';
6
6
  /**
7
7
  * 请求选项.
@@ -67,11 +67,11 @@ export declare function doRequest(opts: HttpRequestOpts): Promise<HttpResponseIn
67
67
  * 发送 json 格式的 post 请求,body 是未序列化的普通对象,表示要发送的请求数据.
68
68
  * @param opts
69
69
  */
70
- export declare function postJson<T>(opts: Pick<HttpRequestOpts, 'url' | 'query' | 'headers' | 'timeout'> & {
71
- body: any;
70
+ export declare function postJson<T>(opts: Pick<HttpRequestOpts, 'url' | 'query' | 'headers' | 'timeout' | 'agent'> & {
71
+ body: unknown;
72
72
  }): Promise<T>;
73
73
  /**
74
74
  * get 请求获取 json 格式数据.
75
75
  * @param opts
76
76
  */
77
- export declare function getJson<T>(opts: Pick<HttpRequestOpts, 'url' | 'query' | 'headers' | 'timeout'>): Promise<T>;
77
+ export declare function getJson<T>(opts: Pick<HttpRequestOpts, 'url' | 'query' | 'headers' | 'timeout' | 'agent'>): Promise<T>;
@@ -11,6 +11,9 @@ export interface LockInfo {
11
11
  /**
12
12
  * 锁管理器,主要用于将不确定的顺序且有冲突的异步操作顺序执行,
13
13
  * 防止异步流程庞大穿插执行造成的数据混乱和错误,常见于请求的处理。
14
+ *
15
+ * ⚠️ 注意:这是一个本地内存锁,仅在当前进程内生效,不支持分布式场景。
16
+ * 如果需要在多进程、多服务器环境下使用,请使用 Redis 等分布式锁方案。
14
17
  */
15
18
  declare class ServerLockManager {
16
19
  /**