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
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.upsertWithUpdater = exports.upsertMany = exports.upsert = void 0;
|
|
4
|
+
const utils_1 = require("../utils");
|
|
5
|
+
const insert_1 = require("./insert");
|
|
6
|
+
const update_1 = require("./update");
|
|
7
|
+
/**
|
|
8
|
+
* Upsert 单条数据
|
|
9
|
+
* 如果主键冲突则更新,否则插入
|
|
10
|
+
* @param config
|
|
11
|
+
* @param connection
|
|
12
|
+
* @param table
|
|
13
|
+
* @param data
|
|
14
|
+
* @returns
|
|
15
|
+
*/
|
|
16
|
+
async function upsert(config, connection, table, data) {
|
|
17
|
+
let columnsSet = new Set();
|
|
18
|
+
if (data[table.id]) {
|
|
19
|
+
columnsSet.add(table.id);
|
|
20
|
+
}
|
|
21
|
+
table.columns.forEach(col => columnsSet.add(col));
|
|
22
|
+
const now = new Date();
|
|
23
|
+
const nowTimestamp = now.getTime();
|
|
24
|
+
if (table.createdDate) {
|
|
25
|
+
const createdData = table.createdDate.type === 'date' ? now : nowTimestamp;
|
|
26
|
+
data[table.createdDate.column] = createdData;
|
|
27
|
+
columnsSet.add(table.createdDate.column);
|
|
28
|
+
}
|
|
29
|
+
if (table.updatedDate) {
|
|
30
|
+
const updatedDate = table.updatedDate.type === 'date' ? now : nowTimestamp;
|
|
31
|
+
data[table.updatedDate.column] = updatedDate;
|
|
32
|
+
columnsSet.add(table.updatedDate.column);
|
|
33
|
+
}
|
|
34
|
+
const columns = Array.from(columnsSet);
|
|
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(',')})`;
|
|
44
|
+
// 构建 on duplicate key update(排除 id)
|
|
45
|
+
const updateColumns = columns.filter(col => col !== table.id);
|
|
46
|
+
const updateFragments = [];
|
|
47
|
+
const updateValues = [];
|
|
48
|
+
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);
|
|
52
|
+
}
|
|
53
|
+
const updateSql = ` on duplicate key update ${updateFragments.join(',')}`;
|
|
54
|
+
const sql = insertSql + updateSql;
|
|
55
|
+
const values = [table.tableName, ...columns, ...insertValues, ...updateValues];
|
|
56
|
+
const res = await (0, utils_1.promiseQuery)(config, connection, sql, values);
|
|
57
|
+
const packet = res;
|
|
58
|
+
if (packet.insertId && (data[table.id] === undefined || data[table.id] === null)) {
|
|
59
|
+
data[table.id] = packet.insertId;
|
|
60
|
+
}
|
|
61
|
+
return data;
|
|
62
|
+
}
|
|
63
|
+
exports.upsert = upsert;
|
|
64
|
+
/**
|
|
65
|
+
* Upsert 多条数据
|
|
66
|
+
* 如果主键冲突则更新,否则插入
|
|
67
|
+
* @param config
|
|
68
|
+
* @param connection
|
|
69
|
+
* @param table
|
|
70
|
+
* @param list
|
|
71
|
+
* @returns 影响的行数
|
|
72
|
+
*/
|
|
73
|
+
async function upsertMany(config, connection, table, list) {
|
|
74
|
+
if (!list.length) {
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
let columnsSet = new Set();
|
|
78
|
+
if (list[0][table.id]) {
|
|
79
|
+
columnsSet.add(table.id);
|
|
80
|
+
}
|
|
81
|
+
table.columns.forEach(col => columnsSet.add(col));
|
|
82
|
+
const now = new Date();
|
|
83
|
+
const nowTimestamp = now.getTime();
|
|
84
|
+
let createdData = undefined;
|
|
85
|
+
if (table.createdDate) {
|
|
86
|
+
createdData = table.createdDate.type === 'date' ? now : nowTimestamp;
|
|
87
|
+
columnsSet.add(table.createdDate.column);
|
|
88
|
+
}
|
|
89
|
+
let updatedDate = undefined;
|
|
90
|
+
if (table.updatedDate) {
|
|
91
|
+
updatedDate = table.updatedDate.type === 'date' ? now : nowTimestamp;
|
|
92
|
+
columnsSet.add(table.updatedDate.column);
|
|
93
|
+
}
|
|
94
|
+
const columns = Array.from(columnsSet);
|
|
95
|
+
let sql = `insert into ??(${columns.map(() => '??').join(',')}) values`;
|
|
96
|
+
const values = [table.tableName, ...columns];
|
|
97
|
+
list.forEach((data, idx) => {
|
|
98
|
+
if (idx > 0) {
|
|
99
|
+
sql += ',';
|
|
100
|
+
}
|
|
101
|
+
const fragList = [];
|
|
102
|
+
const rowValues = [];
|
|
103
|
+
if (table.createdDate) {
|
|
104
|
+
data[table.createdDate.column] = createdData;
|
|
105
|
+
}
|
|
106
|
+
if (table.updatedDate) {
|
|
107
|
+
data[table.updatedDate.column] = updatedDate;
|
|
108
|
+
}
|
|
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);
|
|
116
|
+
});
|
|
117
|
+
const updateColumns = columns.filter(col => col !== table.id);
|
|
118
|
+
sql += ` on duplicate key update ${updateColumns.map(() => '?? = values(??)').join(',')}`;
|
|
119
|
+
updateColumns.forEach(col => values.push(col, col));
|
|
120
|
+
const res = await (0, utils_1.promiseQuery)(config, connection, sql, values);
|
|
121
|
+
const packet = res;
|
|
122
|
+
return packet.affectedRows;
|
|
123
|
+
}
|
|
124
|
+
exports.upsertMany = upsertMany;
|
|
125
|
+
/**
|
|
126
|
+
* Upsert 单条数据(支持自定义更新器)
|
|
127
|
+
* 如果主键冲突则按自定义逻辑更新,否则插入
|
|
128
|
+
* @param config
|
|
129
|
+
* @param connection
|
|
130
|
+
* @param table
|
|
131
|
+
* @param data 插入的数据
|
|
132
|
+
* @param updater 冲突时的更新器
|
|
133
|
+
* @returns
|
|
134
|
+
*/
|
|
135
|
+
async function upsertWithUpdater(config, connection, table, data, updater) {
|
|
136
|
+
let columnsSet = new Set();
|
|
137
|
+
if (data[table.id]) {
|
|
138
|
+
columnsSet.add(table.id);
|
|
139
|
+
}
|
|
140
|
+
table.columns.forEach(col => {
|
|
141
|
+
if (data[col] !== undefined) {
|
|
142
|
+
columnsSet.add(col);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
const now = new Date();
|
|
146
|
+
const nowTimestamp = now.getTime();
|
|
147
|
+
if (table.createdDate) {
|
|
148
|
+
const createdData = table.createdDate.type === 'date' ? now : nowTimestamp;
|
|
149
|
+
data[table.createdDate.column] = createdData;
|
|
150
|
+
columnsSet.add(table.createdDate.column);
|
|
151
|
+
}
|
|
152
|
+
if (table.updatedDate) {
|
|
153
|
+
const updatedDate = table.updatedDate.type === 'date' ? now : nowTimestamp;
|
|
154
|
+
data[table.updatedDate.column] = updatedDate;
|
|
155
|
+
columnsSet.add(table.updatedDate.column);
|
|
156
|
+
}
|
|
157
|
+
const columns = Array.from(columnsSet);
|
|
158
|
+
// 构建 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(',')})`;
|
|
167
|
+
const convertRes = (0, update_1.updatorToSql)(table, updater);
|
|
168
|
+
const updateSql = ` on duplicate key update ${convertRes.sql}`;
|
|
169
|
+
const sql = insertSql + updateSql;
|
|
170
|
+
const values = [table.tableName, ...columns, ...insertValues, ...convertRes.values];
|
|
171
|
+
const res = await (0, utils_1.promiseQuery)(config, connection, sql, values);
|
|
172
|
+
const packet = res;
|
|
173
|
+
if (packet.insertId && (data[table.id] === undefined || data[table.id] === null)) {
|
|
174
|
+
data[table.id] = packet.insertId;
|
|
175
|
+
}
|
|
176
|
+
return data;
|
|
177
|
+
}
|
|
178
|
+
exports.upsertWithUpdater = upsertWithUpdater;
|
|
@@ -6,6 +6,10 @@ exports.processColumnValue = void 0;
|
|
|
6
6
|
* @param value
|
|
7
7
|
*/
|
|
8
8
|
function processColumnValue(value) {
|
|
9
|
+
// undefined/null 返回 null,mysql2 会正确处理为 SQL NULL
|
|
10
|
+
if (value === undefined || value === null) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
9
13
|
// date 类型 typeof 也是 object ,先排除
|
|
10
14
|
if (value instanceof Date) {
|
|
11
15
|
return value;
|
|
@@ -227,12 +227,32 @@ await manager.insert(tableUser, {
|
|
|
227
227
|
nickname: '小明',
|
|
228
228
|
balance: 1
|
|
229
229
|
})
|
|
230
|
+
// Insert with expressions
|
|
231
|
+
await manager.insert(tableUser, {
|
|
232
|
+
id: 'in002',
|
|
233
|
+
nickname: '小红',
|
|
234
|
+
balance: ['expr', '?? * ?', ['score', 2]],
|
|
235
|
+
createAt: ['now']
|
|
236
|
+
})
|
|
230
237
|
// Batch insert
|
|
231
238
|
await manager.insertMany(tableUser, [
|
|
232
239
|
{ id: 'im001', nickname: '张飞', balance: 0 },
|
|
233
240
|
{ id: 'im002', nickname: '关羽', balance: 2 },
|
|
234
241
|
{ id: 'im003', nickname: '刘备', balance: 5 }
|
|
235
242
|
])
|
|
243
|
+
// Upsert single record, update on conflict
|
|
244
|
+
await manager.upsert(tableUser, { id: 'us001', nickname: '赵云', balance: 10 })
|
|
245
|
+
// Batch upsert
|
|
246
|
+
await manager.upsertMany(tableUser, [
|
|
247
|
+
{ id: 'us002', nickname: '马超', balance: 20 },
|
|
248
|
+
{ id: 'us003', nickname: '黄忠', balance: 30 }
|
|
249
|
+
])
|
|
250
|
+
// Upsert with custom updater (e.g., increment balance on conflict)
|
|
251
|
+
await manager.upsertWithUpdater(
|
|
252
|
+
tableUser,
|
|
253
|
+
{ id: 'us001', nickname: '赵云', balance: 10 },
|
|
254
|
+
{ balance: ['inc', 5], nickname: '赵云-updated' }
|
|
255
|
+
)
|
|
236
256
|
// Query first matching record by condition
|
|
237
257
|
const user = await manager.findFirst(tableUser, c =>
|
|
238
258
|
c.like('nickname', 'ff0%').gt('balance', 75).lt('balance', 77)
|
|
@@ -253,6 +273,18 @@ await manager.find({
|
|
|
253
273
|
limit: 10,
|
|
254
274
|
orderBy: [['balance', 'asc']]
|
|
255
275
|
})
|
|
276
|
+
// Custom order by expression: balance * 2 desc
|
|
277
|
+
await manager.find({
|
|
278
|
+
table: tableUser,
|
|
279
|
+
criteria: c => c.like('nickname', 'ff0%'),
|
|
280
|
+
orderBy: [['expr', '?? * ?', ['balance', 2], 'desc']]
|
|
281
|
+
})
|
|
282
|
+
// Custom criteria expression: balance * 2 > 50
|
|
283
|
+
await manager.find({
|
|
284
|
+
table: tableUser,
|
|
285
|
+
criteria: c => c.like('id', 'critex%').expr('?? * ? > ?', ['balance', 2, 50]),
|
|
286
|
+
orderBy: [['balance', 'asc']]
|
|
287
|
+
})
|
|
256
288
|
// Count matching records
|
|
257
289
|
const count = await manager.count(tableUser, c => c.like('id', 'c00%').like('nickname', '李%'))
|
|
258
290
|
// Paginated query
|
|
@@ -295,6 +327,9 @@ await manager.modify(`update user set nickname='无名' where nickname='佚名'`
|
|
|
295
327
|
| findFirst | Query first matching record |
|
|
296
328
|
| insert | Insert record |
|
|
297
329
|
| insertMany | Insert multiple records at once |
|
|
330
|
+
| upsert | Insert record, update on duplicate key |
|
|
331
|
+
| upsertMany | Batch upsert |
|
|
332
|
+
| upsertWithUpdater | Upsert single record with custom updater for conflicts |
|
|
298
333
|
| update | Update record, requires complete information |
|
|
299
334
|
| partialUpdate | Partial update, only provide id and fields to update |
|
|
300
335
|
| updateOne | Update only first matching record, only supports equality conditions |
|
|
@@ -303,9 +338,88 @@ await manager.modify(`update user set nickname='无名' where nickname='佚名'`
|
|
|
303
338
|
| findSelect | Conditional query with specified fields. Same as find but with select param |
|
|
304
339
|
| count | Count matching records. Dangerous operation, use strict conditions |
|
|
305
340
|
| paginate | Paginated query. Dangerous operation, based on find and count |
|
|
341
|
+
| paginateSelect | Paginated query with specified fields, based on findSelect and count |
|
|
306
342
|
| query | Custom SQL query, returns record list, supports prepared statements |
|
|
307
343
|
| modify | Execute custom SQL, returns affected rows, supports prepared statements |
|
|
308
344
|
|
|
345
|
+
### Insert Expressions
|
|
346
|
+
|
|
347
|
+
Starting from version 0.7.0, `insert`, `insertMany`, `upsert` and other insert methods accept `InsertValue` type,
|
|
348
|
+
allowing expressions in the VALUES clause:
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
await manager.insert(tableUser, {
|
|
352
|
+
id: 'in001',
|
|
353
|
+
nickname: '小明',
|
|
354
|
+
// Set to NOW()
|
|
355
|
+
createAt: ['now'],
|
|
356
|
+
// Resolve conflict: set field to the array ['setNull'] (not the setNull operation)
|
|
357
|
+
extra: ['set', ['setNull']],
|
|
358
|
+
// Custom expression: balance = score * 2
|
|
359
|
+
balance: ['expr', '?? * ?', ['score', 2]],
|
|
360
|
+
// Expression without parameters
|
|
361
|
+
balance2: ['expr', 'RAND() * 100']
|
|
362
|
+
})
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Order By Expressions
|
|
366
|
+
|
|
367
|
+
Starting from version 0.7.0, the `orderBy` parameter is upgraded to `OrderBy<T>` type, supporting custom
|
|
368
|
+
expression-based ordering in addition to regular column ordering. This type is backward-compatible.
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
await manager.find({
|
|
372
|
+
table: tableUser,
|
|
373
|
+
criteria: c => c.like('nickname', 'ob%'),
|
|
374
|
+
// Regular column ordering (backward-compatible)
|
|
375
|
+
orderBy: [['balance', 'asc']]
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
// Custom expression ordering: balance * 2 desc
|
|
379
|
+
// SQL: ORDER BY `balance` * 2 desc
|
|
380
|
+
await manager.find({
|
|
381
|
+
table: tableUser,
|
|
382
|
+
criteria: c => c.like('nickname', 'ob%'),
|
|
383
|
+
orderBy: [['expr', '?? * ?', ['balance', 2], 'desc']]
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
// Order by name length: CHAR_LENGTH(name) desc
|
|
387
|
+
// SQL: ORDER BY CHAR_LENGTH(`name`) desc
|
|
388
|
+
await manager.find({
|
|
389
|
+
table: tableBook,
|
|
390
|
+
criteria: c => c.like('name', 'ob%'),
|
|
391
|
+
orderBy: [['expr', 'CHAR_LENGTH(??)', ['name'], 'desc']]
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
// Mixed: regular column + expression
|
|
395
|
+
orderBy: [
|
|
396
|
+
['active', 'asc'],
|
|
397
|
+
['expr', '?? * ?', ['balance', 2], 'desc']
|
|
398
|
+
]
|
|
399
|
+
// SQL: ORDER BY `active` asc , `balance` * 2 desc
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Criteria Expressions
|
|
403
|
+
|
|
404
|
+
Starting from version 0.7.0, `MysqlCriteria` adds an `expr()` method for inserting custom SQL expressions
|
|
405
|
+
in the WHERE clause:
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
// balance * 2 > 50
|
|
409
|
+
// SQL: where ... and `balance` * 2 > 50
|
|
410
|
+
await manager.find({
|
|
411
|
+
table: tableUser,
|
|
412
|
+
criteria: c => c.like('id', 'critex%').expr('?? * ? > ?', ['balance', 2, 50])
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
// Full-text search
|
|
416
|
+
// SQL: where ... and MATCH(`title`, `content`) AGAINST (? IN BOOLEAN MODE)
|
|
417
|
+
await manager.find({
|
|
418
|
+
table: tableBook,
|
|
419
|
+
criteria: c => c.expr('MATCH(??, ??) AGAINST(? IN BOOLEAN MODE)', ['title', 'content', keyword])
|
|
420
|
+
})
|
|
421
|
+
```
|
|
422
|
+
|
|
309
423
|
### JSON Type
|
|
310
424
|
|
|
311
425
|
Version 0.2.0 adds limited support for JSON types. You can insert and query normally, and filtering conditions partially support MySQL's JSON-related functions.
|
|
@@ -427,24 +541,40 @@ The partialUpdate and updateMany methods support partial field modifications and
|
|
|
427
541
|
await manager.updateMany(tableUser, c => c.between('balance', 23, 24), {
|
|
428
542
|
// Increase balance by 2
|
|
429
543
|
balance: ['inc', 2],
|
|
544
|
+
// Increase visits by 1 (default increment)
|
|
545
|
+
visits: ['inc'],
|
|
430
546
|
// Set consume_type to null
|
|
431
|
-
consume_type: ['setNull']
|
|
547
|
+
consume_type: ['setNull'],
|
|
548
|
+
// Set to NOW()
|
|
549
|
+
last_login_at: ['now'],
|
|
550
|
+
// NULL-safe string concatenation: col = CONCAT(IFNULL(col, ''), ?)
|
|
551
|
+
nickname: ['concat', '-suffix'],
|
|
552
|
+
// Custom expression: score = score * 2
|
|
553
|
+
score: ['expr', '?? * ?', ['score', 2]]
|
|
432
554
|
})
|
|
433
555
|
```
|
|
434
556
|
|
|
435
|
-
|
|
557
|
+
> **Note**: `['func']` has been removed in 0.7.0. Use `['expr']` instead.
|
|
558
|
+
> E.g., `['func', 'NOW()']` → `['expr', 'NOW()']`.
|
|
559
|
+
|
|
560
|
+
**Starting from version 0.7.0**, `null` is treated the same as `undefined` and will be ignored during updates. **To set a field to NULL, you must use `['setNull']`**.
|
|
436
561
|
|
|
437
562
|
```ts
|
|
438
563
|
interface User {
|
|
439
564
|
id: string
|
|
440
|
-
// Include null in role type
|
|
441
565
|
role: string | null
|
|
442
566
|
}
|
|
443
567
|
|
|
568
|
+
// Correct: Use ['setNull'] to set field to NULL
|
|
569
|
+
await manager.partialUpdate(tableUser, {
|
|
570
|
+
id: '001',
|
|
571
|
+
role: ['setNull']
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
// Wrong: Starting from 0.7.0, null is ignored and won't update the field
|
|
444
575
|
await manager.partialUpdate(tableUser, {
|
|
445
576
|
id: '001',
|
|
446
|
-
//
|
|
447
|
-
role: null
|
|
577
|
+
role: null // This operation will not take effect
|
|
448
578
|
})
|
|
449
579
|
```
|
|
450
580
|
|
|
@@ -228,12 +228,32 @@ await manager.insert(tableUser, {
|
|
|
228
228
|
nickname: '小明',
|
|
229
229
|
balance: 1
|
|
230
230
|
})
|
|
231
|
+
// 插入时使用表达式
|
|
232
|
+
await manager.insert(tableUser, {
|
|
233
|
+
id: 'in002',
|
|
234
|
+
nickname: '小红',
|
|
235
|
+
balance: ['expr', '?? * ?', ['score', 2]],
|
|
236
|
+
createAt: ['now']
|
|
237
|
+
})
|
|
231
238
|
// 批量插入
|
|
232
239
|
await manager.insertMany(tableUser, [
|
|
233
240
|
{ id: 'im001', nickname: '张飞', balance: 0 },
|
|
234
241
|
{ id: 'im002', nickname: '关羽', balance: 2 },
|
|
235
242
|
{ id: 'im003', nickname: '刘备', balance: 5 }
|
|
236
243
|
])
|
|
244
|
+
// Upsert 单条,id 冲突则更新
|
|
245
|
+
await manager.upsert(tableUser, { id: 'us001', nickname: '赵云', balance: 10 })
|
|
246
|
+
// Upsert 批量
|
|
247
|
+
await manager.upsertMany(tableUser, [
|
|
248
|
+
{ id: 'us002', nickname: '马超', balance: 20 },
|
|
249
|
+
{ id: 'us003', nickname: '黄忠', balance: 30 }
|
|
250
|
+
])
|
|
251
|
+
// Upsert 单条,冲突时自定义更新(如递增余额)
|
|
252
|
+
await manager.upsertWithUpdater(
|
|
253
|
+
tableUser,
|
|
254
|
+
{ id: 'us001', nickname: '赵云', balance: 10 },
|
|
255
|
+
{ balance: ['inc', 5], nickname: '赵云-updated' }
|
|
256
|
+
)
|
|
237
257
|
// 根据指定的条件查询第一条符合的记录
|
|
238
258
|
const user = await manager.findFirst(tableUser, c =>
|
|
239
259
|
c.like('nickname', 'ff0%').gt('balance', 75).lt('balance', 77)
|
|
@@ -254,6 +274,18 @@ await manager.find({
|
|
|
254
274
|
limit: 10,
|
|
255
275
|
orderBy: [['balance', 'asc']]
|
|
256
276
|
})
|
|
277
|
+
// 自定义排序表达式:按 balance * 2 降序
|
|
278
|
+
await manager.find({
|
|
279
|
+
table: tableUser,
|
|
280
|
+
criteria: c => c.like('nickname', 'ff0%'),
|
|
281
|
+
orderBy: [['expr', '?? * ?', ['balance', 2], 'desc']]
|
|
282
|
+
})
|
|
283
|
+
// 自定义查询条件表达式:balance * 2 > 50
|
|
284
|
+
await manager.find({
|
|
285
|
+
table: tableUser,
|
|
286
|
+
criteria: c => c.like('id', 'critex%').expr('?? * ? > ?', ['balance', 2, 50]),
|
|
287
|
+
orderBy: [['balance', 'asc']]
|
|
288
|
+
})
|
|
257
289
|
// 统计符合条件的记录数量
|
|
258
290
|
const count = await manager.count(tableUser, c => c.like('id', 'c00%').like('nickname', '李%'))
|
|
259
291
|
// 分页查询
|
|
@@ -294,18 +326,99 @@ await manager.modify(`update user set nickname='无名' where nickname='佚名'`
|
|
|
294
326
|
| deleteMany | 按指定条件删除,危险操作,建议尽可能设置 limit 参数来限制数量 |
|
|
295
327
|
| findAll | 查询表下所有记录,危险操作,建议只对数据量非常小的表使用 |
|
|
296
328
|
| findFirst | 查询符合条件的第一条记录 |
|
|
297
|
-
| insert
|
|
298
|
-
| insertMany
|
|
299
|
-
|
|
|
300
|
-
|
|
|
301
|
-
|
|
|
302
|
-
|
|
|
303
|
-
|
|
|
304
|
-
|
|
|
305
|
-
|
|
|
306
|
-
|
|
|
307
|
-
|
|
|
308
|
-
|
|
|
329
|
+
| insert | 插入记录 |
|
|
330
|
+
| insertMany | 一次性插入多条记录 |
|
|
331
|
+
| upsert | 插入记录,主键冲突则更新 |
|
|
332
|
+
| upsertMany | 批量 upsert |
|
|
333
|
+
| upsertWithUpdater | upsert 单条,主键冲突时使用自定义更新器(Updater) |
|
|
334
|
+
| update | 更新记录,需要完整信息 |
|
|
335
|
+
| partialUpdate | 局部更新,只提供 id 和需要更新的字段信息 |
|
|
336
|
+
| updateOne | 只更新指定条件的第一条记录,必须是相等条件,不支持范围条件 |
|
|
337
|
+
| updateMany | 更新所有符合条件的记录,危险操作,建议对条件严加限制,控制受影响的范围 |
|
|
338
|
+
| find | 按条件查询所有符合条件的记录,危险操作,建议尽可能设置 limit 参数来限制数量 |
|
|
339
|
+
| findSelect | 指定字段进行条件查询,与 find 唯一的不同的是多一个参数 select 可以用来指定要返回的列 |
|
|
340
|
+
| count | 统计符合条件的记录数量,危险操作,建议严格限制条件,注意索引的利用 |
|
|
341
|
+
| paginate | 分页查询 ,危险操作,基于 find 和 count |
|
|
342
|
+
| paginateSelect | 指定字段进行分页查询,在 paginate 基础上增加了 select 参数 |
|
|
343
|
+
| query | 自定义 sql 查询,返回记录列表,支持预编译 sql |
|
|
344
|
+
| modify | 执行自定义 sql,返回操作记录数 ,支持预编译 sql |
|
|
345
|
+
|
|
346
|
+
### 插入表达式
|
|
347
|
+
|
|
348
|
+
从 0.7.0 版本开始,`insert`、`insertMany`、`upsert` 等插入方法的 data 参数支持 `InsertValue` 类型,
|
|
349
|
+
可以在 VALUES 子句中使用表达式:
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
await manager.insert(tableUser, {
|
|
353
|
+
id: 'in001',
|
|
354
|
+
nickname: '小明',
|
|
355
|
+
// 设置为 NOW()
|
|
356
|
+
createAt: ['now'],
|
|
357
|
+
// 解决冲突:将字段设置为 ['setNull'] 这个数组值(而非执行 setNull 操作)
|
|
358
|
+
extra: ['set', ['setNull']],
|
|
359
|
+
// 自定义表达式:balance = score * 2
|
|
360
|
+
balance: ['expr', '?? * ?', ['score', 2]],
|
|
361
|
+
// 无参数表达式
|
|
362
|
+
balance2: ['expr', 'RAND() * 100']
|
|
363
|
+
})
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### 排序表达式
|
|
367
|
+
|
|
368
|
+
从 0.7.0 版本开始,`orderBy` 参数升级为 `OrderBy<T>` 类型,除了普通列排序外,还支持自定义表达式排序。
|
|
369
|
+
该类型向后兼容,原有的列排序写法不受影响:
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
await manager.find({
|
|
373
|
+
table: tableUser,
|
|
374
|
+
criteria: c => c.like('nickname', 'ob%'),
|
|
375
|
+
// 普通列排序(与旧版兼容)
|
|
376
|
+
orderBy: [['balance', 'asc']]
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
// 自定义表达式排序:按 balance * 2 降序
|
|
380
|
+
// SQL: ORDER BY `balance` * 2 desc
|
|
381
|
+
await manager.find({
|
|
382
|
+
table: tableUser,
|
|
383
|
+
criteria: c => c.like('nickname', 'ob%'),
|
|
384
|
+
orderBy: [['expr', '?? * ?', ['balance', 2], 'desc']]
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
// 按名称长度排序:CHAR_LENGTH(name) desc
|
|
388
|
+
// SQL: ORDER BY CHAR_LENGTH(`name`) desc
|
|
389
|
+
await manager.find({
|
|
390
|
+
table: tableBook,
|
|
391
|
+
criteria: c => c.like('name', 'ob%'),
|
|
392
|
+
orderBy: [['expr', 'CHAR_LENGTH(??)', ['name'], 'desc']]
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
// 混合使用:普通列 + 表达式
|
|
396
|
+
orderBy: [
|
|
397
|
+
['active', 'asc'],
|
|
398
|
+
['expr', '?? * ?', ['balance', 2], 'desc']
|
|
399
|
+
]
|
|
400
|
+
// SQL: ORDER BY `active` asc , `balance` * 2 desc
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### 条件表达式
|
|
404
|
+
|
|
405
|
+
从 0.7.0 版本开始,`MysqlCriteria` 新增 `expr()` 方法,可在 WHERE 子句中插入自定义 SQL 表达式:
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
// balance * 2 > 50
|
|
409
|
+
// SQL: where ... and `balance` * 2 > 50
|
|
410
|
+
await manager.find({
|
|
411
|
+
table: tableUser,
|
|
412
|
+
criteria: c => c.like('id', 'critex%').expr('?? * ? > ?', ['balance', 2, 50])
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
// 全文搜索
|
|
416
|
+
// SQL: where ... and MATCH(`title`, `content`) AGAINST (? IN BOOLEAN MODE)
|
|
417
|
+
await manager.find({
|
|
418
|
+
table: tableBook,
|
|
419
|
+
criteria: c => c.expr('MATCH(??, ??) AGAINST(? IN BOOLEAN MODE)', ['title', 'content', keyword])
|
|
420
|
+
})
|
|
421
|
+
```
|
|
309
422
|
|
|
310
423
|
### json 类型
|
|
311
424
|
|
|
@@ -431,24 +544,40 @@ partialUpdate 和 updateMany 方法支持局部修改一些字段,并且支持
|
|
|
431
544
|
await manager.updateMany(tableUser, c => c.between('balance', 23, 24), {
|
|
432
545
|
// 将 balance 增加 2
|
|
433
546
|
balance: ['inc', 2],
|
|
547
|
+
// 将 balance 增加 1(省略第二个参数,默认 +1)
|
|
548
|
+
visits: ['inc'],
|
|
434
549
|
// 将 consume_type 置空
|
|
435
|
-
consume_type:['setNull']
|
|
550
|
+
consume_type: ['setNull'],
|
|
551
|
+
// 设置为 NOW()
|
|
552
|
+
last_login_at: ['now'],
|
|
553
|
+
// NULL 安全的字符串追加:col = CONCAT(IFNULL(col, ''), ?)
|
|
554
|
+
nickname: ['concat', '-suffix'],
|
|
555
|
+
// 自定义表达式:score = score * 2
|
|
556
|
+
score: ['expr', '?? * ?', ['score', 2]]
|
|
436
557
|
})
|
|
437
558
|
```
|
|
438
559
|
|
|
439
|
-
|
|
560
|
+
> **注意**:0.7.0 版本已移除 `['func']`,请使用 `['expr']` 替代。
|
|
561
|
+
> 如 `['func', 'NOW()']` → `['expr', 'NOW()']`。
|
|
562
|
+
|
|
563
|
+
从 **0.7.0 版本**开始,`null` 和 `undefined` 一样会被忽略更新。**如需将字段设置为 NULL,必须使用 `['setNull']`**。
|
|
440
564
|
|
|
441
565
|
```ts
|
|
442
566
|
interface User {
|
|
443
567
|
id: string
|
|
444
|
-
// 将 role 的类型设置包含 null
|
|
445
568
|
role: string | null
|
|
446
569
|
}
|
|
447
570
|
|
|
571
|
+
// 正确:使用 ['setNull'] 将字段置空
|
|
572
|
+
await manager.partialUpdate(tableUser, {
|
|
573
|
+
id: '001',
|
|
574
|
+
role: ['setNull']
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
// 错误:0.7.0 版本开始,null 会被忽略,不会更新字段
|
|
448
578
|
await manager.partialUpdate(tableUser, {
|
|
449
579
|
id: '001',
|
|
450
|
-
//
|
|
451
|
-
role: null
|
|
580
|
+
role: null // 此操作不会生效
|
|
452
581
|
})
|
|
453
582
|
```
|
|
454
583
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wok-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"packageManager": "pnpm@8.9.0",
|
|
5
5
|
"description": "一个基于 NodeJs 和 TypeScript 的后端框架,轻量级、克制、简洁。A lightweight, restrained, and concise backend framework based on Node.js and TypeScript.",
|
|
6
6
|
"scripts": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"documentation/",
|
|
22
22
|
"skills/",
|
|
23
23
|
"src/",
|
|
24
|
+
"README.en.md",
|
|
24
25
|
"README.md"
|
|
25
26
|
],
|
|
26
27
|
"author": "peak",
|