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.
- package/README.en.md +1 -1
- package/dist/mysql/config.js +1 -1
- package/dist/mysql/manager/base.js +39 -0
- package/dist/mysql/manager/ops/criteria.js +25 -0
- package/dist/mysql/manager/ops/delete.js +4 -10
- package/dist/mysql/manager/ops/find.js +10 -30
- package/dist/mysql/manager/ops/index.js +2 -0
- package/dist/mysql/manager/ops/insert.js +39 -13
- package/dist/mysql/manager/ops/order-by.js +28 -0
- package/dist/mysql/manager/ops/paginate.js +26 -1
- package/dist/mysql/manager/ops/update.js +43 -37
- package/dist/mysql/manager/ops/upsert.js +178 -0
- package/dist/mysql/manager/ops/utils.js +4 -0
- package/documentation/en/mysql.md +135 -5
- package/documentation/zh-cn/mysql.md +146 -17
- package/package.json +2 -1
- package/skills/wok-server-code-navigation/SKILL.md +153 -0
- package/skills/wok-server-mysql/SKILL.md +76 -3
- package/src/mysql/config.ts +2 -2
- package/src/mysql/manager/base.ts +51 -4
- package/src/mysql/manager/ops/criteria.ts +34 -0
- package/src/mysql/manager/ops/delete.ts +5 -10
- package/src/mysql/manager/ops/find.ts +12 -29
- package/src/mysql/manager/ops/index.ts +2 -0
- package/src/mysql/manager/ops/insert.ts +53 -15
- package/src/mysql/manager/ops/order-by.ts +58 -0
- package/src/mysql/manager/ops/paginate.ts +42 -2
- package/src/mysql/manager/ops/update.ts +66 -42
- package/src/mysql/manager/ops/upsert.ts +224 -0
- package/src/mysql/manager/ops/utils.ts +4 -0
- package/types/mysql/config.d.ts +1 -1
- package/types/mysql/manager/base.d.ts +35 -4
- package/types/mysql/manager/ops/criteria.d.ts +10 -0
- package/types/mysql/manager/ops/delete.d.ts +2 -1
- package/types/mysql/manager/ops/find.d.ts +3 -2
- package/types/mysql/manager/ops/index.d.ts +2 -0
- package/types/mysql/manager/ops/insert.d.ts +16 -2
- package/types/mysql/manager/ops/order-by.d.ts +38 -0
- package/types/mysql/manager/ops/paginate.d.ts +18 -1
- package/types/mysql/manager/ops/update.d.ts +26 -3
- package/types/mysql/manager/ops/upsert.d.ts +36 -0
|
@@ -5,6 +5,37 @@ 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
|
+
|
|
8
39
|
/**
|
|
9
40
|
* 为表插入数据
|
|
10
41
|
* @param connection
|
|
@@ -16,9 +47,8 @@ export async function insert<T>(
|
|
|
16
47
|
config: MysqlConfig,
|
|
17
48
|
connection: PoolConnection,
|
|
18
49
|
table: Table<T>,
|
|
19
|
-
data: T
|
|
50
|
+
data: InsertValue<T>
|
|
20
51
|
): Promise<T> {
|
|
21
|
-
// 插入后的新数据
|
|
22
52
|
// 列信息,使用 set 防止 columns 中重复配置 id 和更新创建时间列
|
|
23
53
|
let columnsSet: Set<keyof T> = new Set()
|
|
24
54
|
// 判定下 id ,如果有值,才在 insert 语句中出现 id 列,否则不出现
|
|
@@ -37,15 +67,16 @@ export async function insert<T>(
|
|
|
37
67
|
columnsSet.add(table.updatedDate.column)
|
|
38
68
|
}
|
|
39
69
|
const columns = Array.from(columnsSet)
|
|
40
|
-
// 构建 sql
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
...
|
|
47
|
-
|
|
48
|
-
|
|
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]
|
|
49
80
|
const res = await promiseQuery(config, connection, sql, values)
|
|
50
81
|
const packet = res as ResultSetHeader
|
|
51
82
|
if (packet.affectedRows !== 1) {
|
|
@@ -57,7 +88,7 @@ export async function insert<T>(
|
|
|
57
88
|
if (packet.insertId && (data[table.id] === undefined || data[table.id] === null)) {
|
|
58
89
|
data[table.id] = packet.insertId as any
|
|
59
90
|
}
|
|
60
|
-
return data
|
|
91
|
+
return data as unknown as T
|
|
61
92
|
}
|
|
62
93
|
/**
|
|
63
94
|
* 一次插入多条记录
|
|
@@ -69,7 +100,7 @@ export async function insertMany<T>(
|
|
|
69
100
|
config: MysqlConfig,
|
|
70
101
|
connection: PoolConnection,
|
|
71
102
|
table: Table<T>,
|
|
72
|
-
list: T[]
|
|
103
|
+
list: InsertValue<T>[]
|
|
73
104
|
): Promise<void> {
|
|
74
105
|
if (!list.length) {
|
|
75
106
|
return
|
|
@@ -99,14 +130,21 @@ export async function insertMany<T>(
|
|
|
99
130
|
if (idx > 0) {
|
|
100
131
|
sql += ','
|
|
101
132
|
}
|
|
102
|
-
|
|
133
|
+
const fragList: string[] = []
|
|
134
|
+
const rowValues: any[] = []
|
|
103
135
|
if (table.createdDate) {
|
|
104
136
|
data[table.createdDate.column] = createdData as any
|
|
105
137
|
}
|
|
106
138
|
if (table.updatedDate) {
|
|
107
139
|
data[table.updatedDate.column] = updatedDate as any
|
|
108
140
|
}
|
|
109
|
-
|
|
141
|
+
for (const col of columns) {
|
|
142
|
+
const { frag, values: vs } = processInsertValue(data[col])
|
|
143
|
+
fragList.push(frag)
|
|
144
|
+
rowValues.push(...vs)
|
|
145
|
+
}
|
|
146
|
+
sql += `(${fragList.join(',')})`
|
|
147
|
+
values.push(...rowValues)
|
|
110
148
|
})
|
|
111
149
|
|
|
112
150
|
const res = await promiseQuery(config, connection, sql, values)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 排序规则类型.
|
|
3
|
+
*
|
|
4
|
+
* T 为表类型.
|
|
5
|
+
*
|
|
6
|
+
* 普通列排序:
|
|
7
|
+
* [keyof T, 'asc' | 'desc']
|
|
8
|
+
* 例: ['balance', 'asc'] → ORDER BY `balance` asc
|
|
9
|
+
*
|
|
10
|
+
* 自定义表达式排序:
|
|
11
|
+
* ['expr', SQL片段, 参数值数组, 'asc' | 'desc']
|
|
12
|
+
* 例: ['expr', '?? * ?', ['balance', 2], 'desc']
|
|
13
|
+
* → ORDER BY `balance` * 2 desc
|
|
14
|
+
*
|
|
15
|
+
* 例: ['expr', 'CHAR_LENGTH(??)', ['name'], 'desc']
|
|
16
|
+
* → ORDER BY CHAR_LENGTH(`name`) desc
|
|
17
|
+
*
|
|
18
|
+
* 例: ['expr', 'VECTOR_DISTANCE(??, STRING_TO_VECTOR(?))', ['content_vec', embedding], 'asc']
|
|
19
|
+
* → ORDER BY VECTOR_DISTANCE(`content_vec`, STRING_TO_VECTOR(?)) asc
|
|
20
|
+
*
|
|
21
|
+
* 混合使用:
|
|
22
|
+
* [
|
|
23
|
+
* ['active', 'asc'],
|
|
24
|
+
* ['expr', '?? * ?', ['balance', 2], 'desc']
|
|
25
|
+
* ]
|
|
26
|
+
* → ORDER BY `active` asc , `balance` * 2 desc
|
|
27
|
+
*/
|
|
28
|
+
export type OrderBy<T> = Array<
|
|
29
|
+
| [keyof T, 'asc' | 'desc']
|
|
30
|
+
| ['expr', string, any[], 'asc' | 'desc']
|
|
31
|
+
>
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 构建 ORDER BY 子句.
|
|
35
|
+
*
|
|
36
|
+
* @param orderBy 排序规则
|
|
37
|
+
* @returns { sql: SQL 片段, values: 参数值数组 }
|
|
38
|
+
*/
|
|
39
|
+
export function buildOrderBy<T>(orderBy: OrderBy<T>): { sql: string; values: any[] } {
|
|
40
|
+
const fragments: string[] = []
|
|
41
|
+
const values: any[] = []
|
|
42
|
+
|
|
43
|
+
orderBy.forEach((item, idx) => {
|
|
44
|
+
const prefix = idx === 0 ? ' order by ' : ' , '
|
|
45
|
+
|
|
46
|
+
if (item.length === 4 && item[0] === 'expr') {
|
|
47
|
+
const [, exprSql, exprValues, sort] = item as ['expr', string, any[], 'asc' | 'desc']
|
|
48
|
+
fragments.push(`${prefix}${exprSql} ${sort}`)
|
|
49
|
+
values.push(...exprValues)
|
|
50
|
+
} else {
|
|
51
|
+
const [field, sort] = item as [keyof T, 'asc' | 'desc']
|
|
52
|
+
fragments.push(`${prefix}?? ${sort}`)
|
|
53
|
+
values.push(field)
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
return { sql: fragments.join(''), values }
|
|
58
|
+
}
|
|
@@ -2,8 +2,9 @@ import { PoolConnection } from 'mysql2'
|
|
|
2
2
|
import { Table } from '../../table-info'
|
|
3
3
|
import { count } from './count'
|
|
4
4
|
import { MixCriteria } from './criteria'
|
|
5
|
-
import { find } from './find'
|
|
5
|
+
import { find, findSelect } from './find'
|
|
6
6
|
import { MysqlConfig } from '../../config'
|
|
7
|
+
import { OrderBy } from './order-by'
|
|
7
8
|
|
|
8
9
|
export interface MysqlPaginateOpts<T> {
|
|
9
10
|
/**
|
|
@@ -27,7 +28,7 @@ export interface MysqlPaginateOpts<T> {
|
|
|
27
28
|
/**
|
|
28
29
|
* 排序规则,按先后顺序放入,每个规则是一个元组,第一个元素是字段名称,第二个元素是顺序
|
|
29
30
|
*/
|
|
30
|
-
orderBy?:
|
|
31
|
+
orderBy?: OrderBy<T>
|
|
31
32
|
}
|
|
32
33
|
/**
|
|
33
34
|
* mysql 分页查询结果
|
|
@@ -58,3 +59,42 @@ export async function paginate<T>(
|
|
|
58
59
|
list
|
|
59
60
|
}
|
|
60
61
|
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 指定字段分页查询选项
|
|
65
|
+
*/
|
|
66
|
+
export interface MysqlPaginateSelectOpts<T, K extends keyof T> extends MysqlPaginateOpts<T> {
|
|
67
|
+
/**
|
|
68
|
+
* 要查询的字段
|
|
69
|
+
*/
|
|
70
|
+
select: K[]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 指定字段分页查询
|
|
75
|
+
* @param config
|
|
76
|
+
* @param conn
|
|
77
|
+
* @param opts
|
|
78
|
+
*/
|
|
79
|
+
export async function paginateSelect<T, K extends keyof T>(
|
|
80
|
+
config: MysqlConfig,
|
|
81
|
+
conn: PoolConnection,
|
|
82
|
+
opts: MysqlPaginateSelectOpts<T, K>
|
|
83
|
+
): Promise<MysqlPage<Pick<T, K>>> {
|
|
84
|
+
const pn = opts.pn && opts.pn >= 1 ? opts.pn : 1
|
|
85
|
+
const limit = opts.pz && opts.pz >= 1 && opts.pz <= 1000 ? opts.pz : 20
|
|
86
|
+
const offset = (pn - 1) * limit
|
|
87
|
+
const list = await findSelect(config, conn, {
|
|
88
|
+
table: opts.table,
|
|
89
|
+
criteria: opts.criteria,
|
|
90
|
+
offset,
|
|
91
|
+
limit,
|
|
92
|
+
orderBy: opts.orderBy,
|
|
93
|
+
select: opts.select
|
|
94
|
+
})
|
|
95
|
+
const total = await count(config, conn, opts.table, opts.criteria)
|
|
96
|
+
return {
|
|
97
|
+
total,
|
|
98
|
+
list
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -5,6 +5,7 @@ import { MixCriteria, buildQuery } from './criteria'
|
|
|
5
5
|
import { promiseQuery } from '../utils'
|
|
6
6
|
import { MysqlConfig } from '../../config'
|
|
7
7
|
import { processColumnValue } from './utils'
|
|
8
|
+
import { OrderBy, buildOrderBy } from './order-by'
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* 更新
|
|
@@ -63,57 +64,77 @@ export type Updater<T> = Partial<{
|
|
|
63
64
|
| undefined
|
|
64
65
|
// 将字段置空,置空是不能使用 null 类型的,必须使用元组 ['setNull']
|
|
65
66
|
| ['setNull']
|
|
66
|
-
//
|
|
67
|
+
// 字段自增 +1,等价于 ['inc', 1]
|
|
68
|
+
| ['inc']
|
|
69
|
+
// 字段自增指定值
|
|
67
70
|
| ['inc', number]
|
|
71
|
+
// NOW() 快捷方式,等价于 ['expr', 'NOW()', []]
|
|
72
|
+
| ['now']
|
|
68
73
|
/**
|
|
69
74
|
* 设置一个字段的值,和直接赋值是一样的,作用是解决一些特殊的情况的冲突
|
|
70
75
|
* 比如将 json 字段的值设置为 ['setNull'] ,这就会被认为是要置空,
|
|
71
76
|
* 使用 ['set',['setNull']] 就可以解决这个问题
|
|
72
77
|
*/
|
|
73
78
|
| ['set', T[key]]
|
|
79
|
+
/**
|
|
80
|
+
* 字符串追加,如 ['concat', '/suffix'] 生成 col = CONCAT(IFNULL(col, ''), '/suffix')
|
|
81
|
+
* NULL 安全:字段为 NULL 时视作空字符串
|
|
82
|
+
*/
|
|
83
|
+
| ['concat', string]
|
|
84
|
+
/**
|
|
85
|
+
* 使用自定义表达式
|
|
86
|
+
* 如 ['expr', '?? * ?', ['score', 2]] 生成 score = score * 2
|
|
87
|
+
* 无参数时可省略第三个参数,如 ['expr', 'NOW()'] 等同 ['expr', 'NOW()', []]
|
|
88
|
+
*/
|
|
89
|
+
| ['expr', string, any[]]
|
|
90
|
+
| ['expr', string]
|
|
74
91
|
}>
|
|
75
92
|
/**
|
|
76
93
|
* 转换更新器
|
|
77
94
|
* @param table
|
|
78
95
|
* @param updater
|
|
96
|
+
* @param autoUpdateTime 是否自动添加更新时间
|
|
79
97
|
* @returns
|
|
80
98
|
*/
|
|
81
|
-
function updatorToSql<T>(table: Table<T>, updater: Updater<T>): { sql: string; values: any[] } {
|
|
99
|
+
export function updatorToSql<T>(table: Table<T>, updater: Updater<T>): { sql: string; values: any[] } {
|
|
82
100
|
const values: any[] = []
|
|
83
|
-
// 更新操作
|
|
84
101
|
const updateFragList: string[] = []
|
|
85
|
-
|
|
86
|
-
if (table.updatedDate) {
|
|
87
|
-
const updatedDate = table.updatedDate.type === 'date' ? new Date() : new Date().getTime()
|
|
88
|
-
updateFragList.push(' ?? = ?')
|
|
89
|
-
values.push(table.updatedDate.column, updatedDate)
|
|
90
|
-
}
|
|
102
|
+
|
|
91
103
|
for (const column in updater) {
|
|
92
|
-
// 过滤掉id
|
|
104
|
+
// 过滤掉 id
|
|
93
105
|
if (column === table.id) {
|
|
94
106
|
continue
|
|
95
107
|
}
|
|
108
|
+
// 过滤掉 createdDate / updatedDate(自动处理)
|
|
109
|
+
if ((table.createdDate && column === table.createdDate.column)
|
|
110
|
+
|| (table.updatedDate && column === table.updatedDate.column)) {
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
96
113
|
const val = updater[column]
|
|
97
|
-
// undefined
|
|
114
|
+
// undefined 表示不参与更新
|
|
98
115
|
if (val === undefined) {
|
|
99
116
|
continue
|
|
100
117
|
}
|
|
101
|
-
//
|
|
118
|
+
// 0.7.0 版本开始,null 和 undefined 一样被忽略更新
|
|
119
|
+
// 如需设置字段为 NULL,请使用 ['setNull']
|
|
102
120
|
if (val === null) {
|
|
103
|
-
updateFragList.push(' ?? = NULL ')
|
|
104
|
-
values.push(column)
|
|
105
121
|
continue
|
|
106
122
|
}
|
|
107
123
|
if (Array.isArray(val)) {
|
|
108
|
-
// set null
|
|
109
124
|
if (val[0] === 'setNull') {
|
|
110
125
|
updateFragList.push(' ?? = NULL ')
|
|
111
126
|
values.push(column)
|
|
112
127
|
continue
|
|
113
128
|
}
|
|
114
129
|
if (val[0] === 'inc') {
|
|
130
|
+
const incBy = val.length === 1 ? 1 : val[1]
|
|
115
131
|
updateFragList.push(' ?? = ?? + ? ')
|
|
116
|
-
values.push(column, column,
|
|
132
|
+
values.push(column, column, incBy)
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
if (val[0] === 'now') {
|
|
136
|
+
updateFragList.push(' ?? = NOW() ')
|
|
137
|
+
values.push(column)
|
|
117
138
|
continue
|
|
118
139
|
}
|
|
119
140
|
if (val[0] === 'set') {
|
|
@@ -121,10 +142,35 @@ function updatorToSql<T>(table: Table<T>, updater: Updater<T>): { sql: string; v
|
|
|
121
142
|
values.push(column, processColumnValue(val[1]))
|
|
122
143
|
continue
|
|
123
144
|
}
|
|
145
|
+
if (val[0] === 'concat') {
|
|
146
|
+
updateFragList.push(' ?? = CONCAT(IFNULL(??, \'\'), ?) ')
|
|
147
|
+
values.push(column, column, val[1])
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
if (val[0] === 'expr') {
|
|
151
|
+
updateFragList.push(' ?? = ' + val[1] + ' ')
|
|
152
|
+
values.push(column, ...(val[2] || []))
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
124
155
|
}
|
|
125
156
|
updateFragList.push(' ?? = ? ')
|
|
126
157
|
values.push(column, processColumnValue(val))
|
|
127
158
|
}
|
|
159
|
+
|
|
160
|
+
// 如果没有有效更新字段,抛出异常
|
|
161
|
+
if (updateFragList.length === 0) {
|
|
162
|
+
throw new MysqlException(
|
|
163
|
+
`No effective fields to update (null values are ignored since v0.7.0), table: ${table.tableName}, updater: ${JSON.stringify(updater)}`
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 自动添加更新时间
|
|
168
|
+
if (table.updatedDate) {
|
|
169
|
+
const updatedDate = table.updatedDate.type === 'date' ? new Date() : new Date().getTime()
|
|
170
|
+
updateFragList.push(' ?? = ?')
|
|
171
|
+
values.push(table.updatedDate.column, updatedDate)
|
|
172
|
+
}
|
|
173
|
+
|
|
128
174
|
return { sql: updateFragList.join(','), values }
|
|
129
175
|
}
|
|
130
176
|
|
|
@@ -152,13 +198,6 @@ export async function partialUpdate<T>(
|
|
|
152
198
|
if (typeof id !== 'string' && typeof id !== 'number') {
|
|
153
199
|
throw new MysqlException('Primary key can only be of string or number type')
|
|
154
200
|
}
|
|
155
|
-
if (Object.keys(data).length < 2) {
|
|
156
|
-
throw new MysqlException(
|
|
157
|
-
`Can't do a partial update, data must contain at least one column outside of the primary key,table: ${
|
|
158
|
-
table.tableName
|
|
159
|
-
},column:${JSON.stringify(data)}`
|
|
160
|
-
)
|
|
161
|
-
}
|
|
162
201
|
const fieldNames = Object.keys(data)
|
|
163
202
|
for (const name of fieldNames) {
|
|
164
203
|
if (name !== table.id && !table.columns.some(col => col === name)) {
|
|
@@ -171,9 +210,6 @@ export async function partialUpdate<T>(
|
|
|
171
210
|
const values: any[] = [table.tableName]
|
|
172
211
|
// 更新操作
|
|
173
212
|
const convertRes = updatorToSql(table, data)
|
|
174
|
-
if (!convertRes.sql) {
|
|
175
|
-
throw new MysqlException('No fields were specified to be updated!')
|
|
176
|
-
}
|
|
177
213
|
values.push(...convertRes.values)
|
|
178
214
|
sql += ` set ${convertRes.sql} where ?? = ?`
|
|
179
215
|
values.push(table.id, id)
|
|
@@ -206,9 +242,6 @@ export async function updateOne<T>(
|
|
|
206
242
|
values.push(table.tableName)
|
|
207
243
|
// 更新操作
|
|
208
244
|
const convertRes = updatorToSql(table, updater)
|
|
209
|
-
if (!convertRes.sql) {
|
|
210
|
-
throw new MysqlException('No fields were specified to be updated!')
|
|
211
|
-
}
|
|
212
245
|
sql += ` set ${convertRes.sql} `
|
|
213
246
|
values.push(...convertRes.values)
|
|
214
247
|
sql += ` where ${mysqlQuery.sql} limit 1`
|
|
@@ -236,7 +269,7 @@ export interface UpdateOpts<T> {
|
|
|
236
269
|
/**
|
|
237
270
|
* 排序规则,按先后顺序放入,每个规则是一个元组,第一个元素是字段名称,第二个元素是顺序
|
|
238
271
|
*/
|
|
239
|
-
orderBy?:
|
|
272
|
+
orderBy?: OrderBy<T>
|
|
240
273
|
/**
|
|
241
274
|
* 更新设置
|
|
242
275
|
*/
|
|
@@ -265,24 +298,15 @@ export async function updateMany<T>(
|
|
|
265
298
|
values.push(opts.table.tableName)
|
|
266
299
|
// 更新操作
|
|
267
300
|
const convertRes = updatorToSql(opts.table, opts.updater)
|
|
268
|
-
if (!convertRes.sql) {
|
|
269
|
-
throw new MysqlException('No fields were specified to be updated!')
|
|
270
|
-
}
|
|
271
301
|
sql += ` set ${convertRes.sql} `
|
|
272
302
|
values.push(...convertRes.values)
|
|
273
303
|
sql += ` where ${mysqlQuery.sql} `
|
|
274
304
|
values.push(...mysqlQuery.values)
|
|
275
305
|
// 排序
|
|
276
306
|
if (opts.orderBy && opts.orderBy.length) {
|
|
277
|
-
opts.orderBy
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
sql += ` order by ?? ${sort} `
|
|
281
|
-
} else {
|
|
282
|
-
sql += ` , ?? ${sort} `
|
|
283
|
-
}
|
|
284
|
-
values.push(field)
|
|
285
|
-
})
|
|
307
|
+
const ob = buildOrderBy(opts.orderBy)
|
|
308
|
+
sql += ob.sql
|
|
309
|
+
values.push(...ob.values)
|
|
286
310
|
}
|
|
287
311
|
// 数量限制
|
|
288
312
|
if (opts.limit) {
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { PoolConnection, ResultSetHeader } from 'mysql2'
|
|
2
|
+
import { MysqlConfig } from '../../config'
|
|
3
|
+
import { Table } from '../../table-info'
|
|
4
|
+
import { promiseQuery } from '../utils'
|
|
5
|
+
import { InsertValue, processInsertValue } from './insert'
|
|
6
|
+
import { Updater, updatorToSql } from './update'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Upsert 单条数据
|
|
10
|
+
* 如果主键冲突则更新,否则插入
|
|
11
|
+
* @param config
|
|
12
|
+
* @param connection
|
|
13
|
+
* @param table
|
|
14
|
+
* @param data
|
|
15
|
+
* @returns
|
|
16
|
+
*/
|
|
17
|
+
export async function upsert<T>(
|
|
18
|
+
config: MysqlConfig,
|
|
19
|
+
connection: PoolConnection,
|
|
20
|
+
table: Table<T>,
|
|
21
|
+
data: InsertValue<T>
|
|
22
|
+
): Promise<T> {
|
|
23
|
+
let columnsSet: Set<keyof T> = new Set()
|
|
24
|
+
if (data[table.id]) {
|
|
25
|
+
columnsSet.add(table.id)
|
|
26
|
+
}
|
|
27
|
+
table.columns.forEach(col => columnsSet.add(col))
|
|
28
|
+
|
|
29
|
+
const now = new Date()
|
|
30
|
+
const nowTimestamp = now.getTime()
|
|
31
|
+
|
|
32
|
+
if (table.createdDate) {
|
|
33
|
+
const createdData = table.createdDate.type === 'date' ? now : nowTimestamp
|
|
34
|
+
data[table.createdDate.column] = createdData as any
|
|
35
|
+
columnsSet.add(table.createdDate.column)
|
|
36
|
+
}
|
|
37
|
+
if (table.updatedDate) {
|
|
38
|
+
const updatedDate = table.updatedDate.type === 'date' ? now : nowTimestamp
|
|
39
|
+
data[table.updatedDate.column] = updatedDate as any
|
|
40
|
+
columnsSet.add(table.updatedDate.column)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const columns = Array.from(columnsSet)
|
|
44
|
+
|
|
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(',')})`
|
|
54
|
+
|
|
55
|
+
// 构建 on duplicate key update(排除 id)
|
|
56
|
+
const updateColumns = columns.filter(col => col !== table.id)
|
|
57
|
+
const updateFragments: string[] = []
|
|
58
|
+
const updateValues: any[] = []
|
|
59
|
+
for (const col of updateColumns) {
|
|
60
|
+
const { frag, values: vs } = processInsertValue(data[col])
|
|
61
|
+
updateFragments.push(`?? = ${frag}`)
|
|
62
|
+
updateValues.push(col, ...vs)
|
|
63
|
+
}
|
|
64
|
+
const updateSql = ` on duplicate key update ${updateFragments.join(',')}`
|
|
65
|
+
|
|
66
|
+
const sql = insertSql + updateSql
|
|
67
|
+
|
|
68
|
+
const values = [table.tableName, ...columns, ...insertValues, ...updateValues]
|
|
69
|
+
|
|
70
|
+
const res = await promiseQuery(config, connection, sql, values)
|
|
71
|
+
const packet = res as ResultSetHeader
|
|
72
|
+
|
|
73
|
+
if (packet.insertId && (data[table.id] === undefined || data[table.id] === null)) {
|
|
74
|
+
data[table.id] = packet.insertId as any
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return data as unknown as T
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Upsert 多条数据
|
|
82
|
+
* 如果主键冲突则更新,否则插入
|
|
83
|
+
* @param config
|
|
84
|
+
* @param connection
|
|
85
|
+
* @param table
|
|
86
|
+
* @param list
|
|
87
|
+
* @returns 影响的行数
|
|
88
|
+
*/
|
|
89
|
+
export async function upsertMany<T>(
|
|
90
|
+
config: MysqlConfig,
|
|
91
|
+
connection: PoolConnection,
|
|
92
|
+
table: Table<T>,
|
|
93
|
+
list: InsertValue<T>[]
|
|
94
|
+
): Promise<number> {
|
|
95
|
+
if (!list.length) {
|
|
96
|
+
return 0
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let columnsSet: Set<keyof T> = new Set()
|
|
100
|
+
if (list[0][table.id]) {
|
|
101
|
+
columnsSet.add(table.id)
|
|
102
|
+
}
|
|
103
|
+
table.columns.forEach(col => columnsSet.add(col))
|
|
104
|
+
|
|
105
|
+
const now = new Date()
|
|
106
|
+
const nowTimestamp = now.getTime()
|
|
107
|
+
|
|
108
|
+
let createdData: Date | number | undefined = undefined
|
|
109
|
+
if (table.createdDate) {
|
|
110
|
+
createdData = table.createdDate.type === 'date' ? now : nowTimestamp
|
|
111
|
+
columnsSet.add(table.createdDate.column)
|
|
112
|
+
}
|
|
113
|
+
let updatedDate: Date | number | undefined = undefined
|
|
114
|
+
if (table.updatedDate) {
|
|
115
|
+
updatedDate = table.updatedDate.type === 'date' ? now : nowTimestamp
|
|
116
|
+
columnsSet.add(table.updatedDate.column)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const columns = Array.from(columnsSet)
|
|
120
|
+
|
|
121
|
+
let sql = `insert into ??(${columns.map(() => '??').join(',')}) values`
|
|
122
|
+
const values: any[] = [table.tableName, ...columns]
|
|
123
|
+
|
|
124
|
+
list.forEach((data, idx) => {
|
|
125
|
+
if (idx > 0) {
|
|
126
|
+
sql += ','
|
|
127
|
+
}
|
|
128
|
+
const fragList: string[] = []
|
|
129
|
+
const rowValues: any[] = []
|
|
130
|
+
if (table.createdDate) {
|
|
131
|
+
data[table.createdDate.column] = createdData as any
|
|
132
|
+
}
|
|
133
|
+
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)
|
|
140
|
+
}
|
|
141
|
+
sql += `(${fragList.join(',')})`
|
|
142
|
+
values.push(...rowValues)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
const updateColumns = columns.filter(col => col !== table.id)
|
|
146
|
+
sql += ` on duplicate key update ${updateColumns.map(() => '?? = values(??)').join(',')}`
|
|
147
|
+
updateColumns.forEach(col => values.push(col, col))
|
|
148
|
+
|
|
149
|
+
const res = await promiseQuery(config, connection, sql, values)
|
|
150
|
+
const packet = res as ResultSetHeader
|
|
151
|
+
|
|
152
|
+
return packet.affectedRows
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Upsert 单条数据(支持自定义更新器)
|
|
157
|
+
* 如果主键冲突则按自定义逻辑更新,否则插入
|
|
158
|
+
* @param config
|
|
159
|
+
* @param connection
|
|
160
|
+
* @param table
|
|
161
|
+
* @param data 插入的数据
|
|
162
|
+
* @param updater 冲突时的更新器
|
|
163
|
+
* @returns
|
|
164
|
+
*/
|
|
165
|
+
export async function upsertWithUpdater<T>(
|
|
166
|
+
config: MysqlConfig,
|
|
167
|
+
connection: PoolConnection,
|
|
168
|
+
table: Table<T>,
|
|
169
|
+
data: InsertValue<T>,
|
|
170
|
+
updater: Updater<T>
|
|
171
|
+
): Promise<T> {
|
|
172
|
+
let columnsSet: Set<keyof T> = new Set()
|
|
173
|
+
if (data[table.id]) {
|
|
174
|
+
columnsSet.add(table.id)
|
|
175
|
+
}
|
|
176
|
+
table.columns.forEach(col => {
|
|
177
|
+
if (data[col] !== undefined) {
|
|
178
|
+
columnsSet.add(col)
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
const now = new Date()
|
|
183
|
+
const nowTimestamp = now.getTime()
|
|
184
|
+
|
|
185
|
+
if (table.createdDate) {
|
|
186
|
+
const createdData = table.createdDate.type === 'date' ? now : nowTimestamp
|
|
187
|
+
data[table.createdDate.column] = createdData as any
|
|
188
|
+
columnsSet.add(table.createdDate.column)
|
|
189
|
+
}
|
|
190
|
+
if (table.updatedDate) {
|
|
191
|
+
const updatedDate = table.updatedDate.type === 'date' ? now : nowTimestamp
|
|
192
|
+
data[table.updatedDate.column] = updatedDate as any
|
|
193
|
+
columnsSet.add(table.updatedDate.column)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const columns = Array.from(columnsSet)
|
|
197
|
+
|
|
198
|
+
// 构建 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(',')})`
|
|
207
|
+
|
|
208
|
+
const convertRes = updatorToSql(table, updater)
|
|
209
|
+
|
|
210
|
+
const updateSql = ` on duplicate key update ${convertRes.sql}`
|
|
211
|
+
|
|
212
|
+
const sql = insertSql + updateSql
|
|
213
|
+
|
|
214
|
+
const values = [table.tableName, ...columns, ...insertValues, ...convertRes.values]
|
|
215
|
+
|
|
216
|
+
const res = await promiseQuery(config, connection, sql, values)
|
|
217
|
+
const packet = res as ResultSetHeader
|
|
218
|
+
|
|
219
|
+
if (packet.insertId && (data[table.id] === undefined || data[table.id] === null)) {
|
|
220
|
+
data[table.id] = packet.insertId as any
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return data as unknown as T
|
|
224
|
+
}
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
* @param value
|
|
4
4
|
*/
|
|
5
5
|
export function processColumnValue(value: any) {
|
|
6
|
+
// undefined/null 返回 null,mysql2 会正确处理为 SQL NULL
|
|
7
|
+
if (value === undefined || value === null) {
|
|
8
|
+
return null
|
|
9
|
+
}
|
|
6
10
|
// date 类型 typeof 也是 object ,先排除
|
|
7
11
|
if (value instanceof Date) {
|
|
8
12
|
return value
|