wok-server 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.en.md +1 -1
  2. package/dist/mysql/config.js +1 -1
  3. package/dist/mysql/manager/base.js +39 -0
  4. package/dist/mysql/manager/ops/criteria.js +25 -0
  5. package/dist/mysql/manager/ops/delete.js +4 -10
  6. package/dist/mysql/manager/ops/find.js +10 -30
  7. package/dist/mysql/manager/ops/index.js +2 -0
  8. package/dist/mysql/manager/ops/insert.js +39 -13
  9. package/dist/mysql/manager/ops/order-by.js +28 -0
  10. package/dist/mysql/manager/ops/paginate.js +26 -1
  11. package/dist/mysql/manager/ops/update.js +43 -37
  12. package/dist/mysql/manager/ops/upsert.js +178 -0
  13. package/dist/mysql/manager/ops/utils.js +4 -0
  14. package/documentation/en/mysql.md +135 -5
  15. package/documentation/zh-cn/mysql.md +146 -17
  16. package/package.json +2 -1
  17. package/skills/wok-server-code-navigation/SKILL.md +153 -0
  18. package/skills/wok-server-mysql/SKILL.md +76 -3
  19. package/src/mysql/config.ts +2 -2
  20. package/src/mysql/manager/base.ts +51 -4
  21. package/src/mysql/manager/ops/criteria.ts +34 -0
  22. package/src/mysql/manager/ops/delete.ts +5 -10
  23. package/src/mysql/manager/ops/find.ts +12 -29
  24. package/src/mysql/manager/ops/index.ts +2 -0
  25. package/src/mysql/manager/ops/insert.ts +53 -15
  26. package/src/mysql/manager/ops/order-by.ts +58 -0
  27. package/src/mysql/manager/ops/paginate.ts +42 -2
  28. package/src/mysql/manager/ops/update.ts +66 -42
  29. package/src/mysql/manager/ops/upsert.ts +224 -0
  30. package/src/mysql/manager/ops/utils.ts +4 -0
  31. package/types/mysql/config.d.ts +1 -1
  32. package/types/mysql/manager/base.d.ts +35 -4
  33. package/types/mysql/manager/ops/criteria.d.ts +10 -0
  34. package/types/mysql/manager/ops/delete.d.ts +2 -1
  35. package/types/mysql/manager/ops/find.d.ts +3 -2
  36. package/types/mysql/manager/ops/index.d.ts +2 -0
  37. package/types/mysql/manager/ops/insert.d.ts +16 -2
  38. package/types/mysql/manager/ops/order-by.d.ts +38 -0
  39. package/types/mysql/manager/ops/paginate.d.ts +18 -1
  40. package/types/mysql/manager/ops/update.d.ts +26 -3
  41. package/types/mysql/manager/ops/upsert.d.ts +36 -0
@@ -0,0 +1,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
- Setting a field to null directly also works, but requires TypeScript type support:
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
- // Equivalent to role: ['setNull']
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
- | update | 更新记录,需要完整信息 |
300
- | partialUpdate | 局部更新,只提供 id 和需要更新的字段信息 |
301
- | updateOne | 只更新指定条件的第一条记录,必须是相等条件,不支持范围条件 |
302
- | updateMany | 更新所有符合条件的记录,危险操作,建议对条件严加限制,控制受影响的范围 |
303
- | find | 按条件查询所有符合条件的记录,危险操作,建议尽可能设置 limit 参数来限制数量 |
304
- | findSelect | 指定字段进行条件查询,与 find 唯一的不同的是多一个参数 select 可以用来指定要返回的列 |
305
- | count | 统计符合条件的记录数量,危险操作,建议严格限制条件,注意索引的利用 |
306
- | paginate | 分页查询 ,危险操作,基于 find 和 count |
307
- | query | 自定义 sql 查询,返回记录列表,支持预编译 sql |
308
- | modify | 执行自定义 sql,返回操作记录数 ,支持预编译 sql |
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
- 如果将一个字段的值设置为 null 也可以置空,但是这要求设置 ts 类型支持。
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
- // 等同于 role:['setNull']
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.6.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",