@maiyunnet/kebab 9.4.1 → 9.6.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/doc/kebab-rag.md +187 -143
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/lib/core.d.ts +33 -9
- package/lib/core.js +38 -19
- package/lib/sql.d.ts +15 -0
- package/lib/sql.js +95 -0
- package/package.json +1 -1
- package/sys/master.js +1 -1
- package/sys/mod.d.ts +4 -2
- package/sys/mod.js +34 -55
- package/www/example/ctr/test.js +10 -7
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* --- 本文件用来定义每个目录实体地址的常量 ---
|
|
7
7
|
*/
|
|
8
8
|
/** --- 当前系统版本号 --- */
|
|
9
|
-
export const VER = '9.
|
|
9
|
+
export const VER = '9.6.0';
|
|
10
10
|
// --- 服务端用的路径 ---
|
|
11
11
|
const imu = decodeURIComponent(import.meta.url).replace('file://', '').replace(/^\/(\w:)/, '$1');
|
|
12
12
|
/** --- /xxx/xxx --- */
|
package/lib/core.d.ts
CHANGED
|
@@ -167,12 +167,18 @@ export declare function exec(command: string, options?: {
|
|
|
167
167
|
* --- 向主进程(或局域网同代码机子)发送广播将进行 reload 操作,等待回传 ---
|
|
168
168
|
* --- 主要作用除代码热更新以外的其他情况 ---
|
|
169
169
|
*/
|
|
170
|
-
export declare function sendReload(hosts?: string[] | 'config'): Promise<string
|
|
170
|
+
export declare function sendReload(hosts?: string[] | 'config'): Promise<Record<string, {
|
|
171
|
+
'result': boolean;
|
|
172
|
+
'return': string;
|
|
173
|
+
}>>;
|
|
171
174
|
/**
|
|
172
175
|
* --- 向主进程(或局域网同代码机子)发送广播将进行 restart 操作,停止监听并启动新进程,老进程在连接全部断开后自行销毁 ---
|
|
173
176
|
* --- 主要用作不间断的代码热更新 ---
|
|
174
177
|
*/
|
|
175
|
-
export declare function sendRestart(hosts?: string[] | 'config'): Promise<string
|
|
178
|
+
export declare function sendRestart(hosts?: string[] | 'config'): Promise<Record<string, {
|
|
179
|
+
'result': boolean;
|
|
180
|
+
'return': string;
|
|
181
|
+
}>>;
|
|
176
182
|
/** --- PM2 操作类型 --- */
|
|
177
183
|
export type TPm2Action = 'start' | 'stop' | 'restart';
|
|
178
184
|
/**
|
|
@@ -181,13 +187,19 @@ export type TPm2Action = 'start' | 'stop' | 'restart';
|
|
|
181
187
|
* @param action PM2 操作类型
|
|
182
188
|
* @param hosts 局域网列表
|
|
183
189
|
*/
|
|
184
|
-
export declare function sendPm2(name: string, action?: TPm2Action, hosts?: string[] | 'config'): Promise<string
|
|
190
|
+
export declare function sendPm2(name: string, action?: TPm2Action, hosts?: string[] | 'config'): Promise<Record<string, {
|
|
191
|
+
'result': boolean;
|
|
192
|
+
'return': string;
|
|
193
|
+
}>>;
|
|
185
194
|
/**
|
|
186
195
|
* --- 向本机或局域网 RPC 发送 npm install 操作 ---
|
|
187
196
|
* @param path 路径,如 /home/kebab/
|
|
188
|
-
* @param hosts
|
|
197
|
+
* @param hosts 局域网列表,不填则代表本机
|
|
189
198
|
*/
|
|
190
|
-
export declare function sendNpm(path: string, hosts?: string[] | 'config'): Promise<string
|
|
199
|
+
export declare function sendNpm(path: string, hosts?: string[] | 'config'): Promise<Record<string, {
|
|
200
|
+
'result': boolean;
|
|
201
|
+
'return': string;
|
|
202
|
+
}>>;
|
|
191
203
|
/** --- 跨进程全局变量 --- */
|
|
192
204
|
export declare const global: Record<string, any>;
|
|
193
205
|
/**
|
|
@@ -196,13 +208,19 @@ export declare const global: Record<string, any>;
|
|
|
196
208
|
* @param data 变量值
|
|
197
209
|
* @param hosts 局域网列表
|
|
198
210
|
*/
|
|
199
|
-
export declare function setGlobal(key: string, data: any, hosts?: string[] | 'config'): Promise<string
|
|
211
|
+
export declare function setGlobal(key: string, data: any, hosts?: string[] | 'config'): Promise<Record<string, {
|
|
212
|
+
'result': boolean;
|
|
213
|
+
'return': string;
|
|
214
|
+
}>>;
|
|
200
215
|
/**
|
|
201
216
|
* --- 移除某个跨线程/跨内网服务器全局变量 ---
|
|
202
217
|
* @param key 变量名
|
|
203
218
|
* @param hosts 局域网列表
|
|
204
219
|
*/
|
|
205
|
-
export declare function removeGlobal(key: string, hosts?: string[]): Promise<string
|
|
220
|
+
export declare function removeGlobal(key: string, hosts?: string[] | 'config'): Promise<Record<string, {
|
|
221
|
+
'result': boolean;
|
|
222
|
+
'return': string;
|
|
223
|
+
}>>;
|
|
206
224
|
/**
|
|
207
225
|
* --- 上传并覆盖代码文件,config.json、kebab.json、.js.map、.ts, .gitignore 不会被覆盖和新创建 ---
|
|
208
226
|
* @param sourcePath zip 文件
|
|
@@ -222,13 +240,19 @@ export declare function updateCode(sourcePath: string, path: string, hosts?: str
|
|
|
222
240
|
* @param value 要更新的值
|
|
223
241
|
* @param hosts 局域网列表
|
|
224
242
|
*/
|
|
225
|
-
export declare function sendProject(path: string, key: string, value: string, hosts?: string[] | 'config'): Promise<string
|
|
243
|
+
export declare function sendProject(path: string, key: string, value: string, hosts?: string[] | 'config'): Promise<Record<string, {
|
|
244
|
+
'result': boolean;
|
|
245
|
+
'return': string;
|
|
246
|
+
}>>;
|
|
226
247
|
/**
|
|
227
248
|
* --- 向本机或局域网 RPC 发送 package.json 更新操作 ---
|
|
228
249
|
* @param content package.json 文件内容
|
|
229
250
|
* @param hosts 局域网列表
|
|
230
251
|
*/
|
|
231
|
-
export declare function sendPackage(content: string, hosts?: string[] | 'config'): Promise<string
|
|
252
|
+
export declare function sendPackage(content: string, hosts?: string[] | 'config'): Promise<Record<string, {
|
|
253
|
+
'result': boolean;
|
|
254
|
+
'return': string;
|
|
255
|
+
}>>;
|
|
232
256
|
/** --- log 设置的选项 --- */
|
|
233
257
|
export interface ILogOptions {
|
|
234
258
|
'path'?: string;
|
package/lib/core.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Project: Kebab, User: JianSuoQiYue
|
|
3
3
|
* Date: 2019-5-3 23:54
|
|
4
|
-
* Last: 2020-4-11 22:34:58, 2022-10-2 14:13:06, 2022-12-28 20:33:24, 2023-12-15 11:49:02, 2024-7-2 15:23:35, 2025-6-13 19:45:53
|
|
4
|
+
* Last: 2020-4-11 22:34:58, 2022-10-2 14:13:06, 2022-12-28 20:33:24, 2023-12-15 11:49:02, 2024-7-2 15:23:35, 2025-6-13 19:45:53, 2026-05-20 09:50:00
|
|
5
5
|
*/
|
|
6
6
|
import * as cp from 'child_process';
|
|
7
7
|
import * as http2 from 'http2';
|
|
@@ -464,7 +464,9 @@ export async function sendReload(hosts) {
|
|
|
464
464
|
process.send({
|
|
465
465
|
'action': 'reload'
|
|
466
466
|
});
|
|
467
|
-
return
|
|
467
|
+
return {
|
|
468
|
+
'127.0.0.1': { 'result': true, 'return': 'Done' }
|
|
469
|
+
};
|
|
468
470
|
}
|
|
469
471
|
if (hosts === 'config') {
|
|
470
472
|
hosts = globalConfig.hosts;
|
|
@@ -472,7 +474,7 @@ export async function sendReload(hosts) {
|
|
|
472
474
|
// --- 局域网模式 ---
|
|
473
475
|
const time = lTime.stamp();
|
|
474
476
|
/** --- 返回成功的 host --- */
|
|
475
|
-
const rtn =
|
|
477
|
+
const rtn = {};
|
|
476
478
|
for (const host of hosts) {
|
|
477
479
|
const res = await lUndici.get('http://' + host + ':' + globalConfig.rpcPort.toString() + '/' + lCrypto.aesEncrypt(lText.stringifyJson({
|
|
478
480
|
'action': 'reload',
|
|
@@ -486,7 +488,10 @@ export async function sendReload(hosts) {
|
|
|
486
488
|
}
|
|
487
489
|
const str = content.toString();
|
|
488
490
|
if (str === 'Done') {
|
|
489
|
-
rtn
|
|
491
|
+
rtn[host] = { 'result': true, 'return': 'Done' };
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
rtn[host] = { 'result': false, 'return': str };
|
|
490
495
|
}
|
|
491
496
|
}
|
|
492
497
|
return rtn;
|
|
@@ -503,7 +508,9 @@ export async function sendRestart(hosts) {
|
|
|
503
508
|
process.send({
|
|
504
509
|
'action': 'restart'
|
|
505
510
|
});
|
|
506
|
-
return
|
|
511
|
+
return {
|
|
512
|
+
'127.0.0.1': { 'result': true, 'return': 'Done' }
|
|
513
|
+
};
|
|
507
514
|
}
|
|
508
515
|
if (hosts === 'config') {
|
|
509
516
|
hosts = globalConfig.hosts;
|
|
@@ -511,7 +518,7 @@ export async function sendRestart(hosts) {
|
|
|
511
518
|
// --- 局域网模式 ---
|
|
512
519
|
const time = lTime.stamp();
|
|
513
520
|
/** --- 返回成功的 host --- */
|
|
514
|
-
const rtn =
|
|
521
|
+
const rtn = {};
|
|
515
522
|
for (const host of hosts) {
|
|
516
523
|
const res = await lUndici.get('http://' + host + ':' + globalConfig.rpcPort.toString() + '/' + lCrypto.aesEncrypt(lText.stringifyJson({
|
|
517
524
|
'action': 'restart',
|
|
@@ -525,7 +532,10 @@ export async function sendRestart(hosts) {
|
|
|
525
532
|
}
|
|
526
533
|
const str = content.toString();
|
|
527
534
|
if (str === 'Done') {
|
|
528
|
-
rtn
|
|
535
|
+
rtn[host] = { 'result': true, 'return': 'Done' };
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
rtn[host] = { 'result': false, 'return': str };
|
|
529
539
|
}
|
|
530
540
|
}
|
|
531
541
|
return rtn;
|
|
@@ -547,7 +557,7 @@ export async function sendPm2(name, action = 'restart', hosts) {
|
|
|
547
557
|
// --- 局域网模式 ---
|
|
548
558
|
const time = lTime.stamp();
|
|
549
559
|
/** --- 返回成功的 host --- */
|
|
550
|
-
const rtn =
|
|
560
|
+
const rtn = {};
|
|
551
561
|
for (const host of hosts) {
|
|
552
562
|
const res = await lUndici.get('http://' + host + ':' + globalConfig.rpcPort.toString() + '/' + lCrypto.aesEncrypt(lText.stringifyJson({
|
|
553
563
|
'action': 'pm2',
|
|
@@ -563,9 +573,10 @@ export async function sendPm2(name, action = 'restart', hosts) {
|
|
|
563
573
|
}
|
|
564
574
|
const str = content.toString();
|
|
565
575
|
if (str === 'Done') {
|
|
566
|
-
rtn
|
|
576
|
+
rtn[host] = { 'result': true, 'return': 'Done' };
|
|
567
577
|
}
|
|
568
578
|
else {
|
|
579
|
+
rtn[host] = { 'result': false, 'return': str };
|
|
569
580
|
debug('[CORE][sendPm2] rpc server content error:', str);
|
|
570
581
|
}
|
|
571
582
|
}
|
|
@@ -574,7 +585,7 @@ export async function sendPm2(name, action = 'restart', hosts) {
|
|
|
574
585
|
/**
|
|
575
586
|
* --- 向本机或局域网 RPC 发送 npm install 操作 ---
|
|
576
587
|
* @param path 路径,如 /home/kebab/
|
|
577
|
-
* @param hosts
|
|
588
|
+
* @param hosts 局域网列表,不填则代表本机
|
|
578
589
|
*/
|
|
579
590
|
export async function sendNpm(path, hosts) {
|
|
580
591
|
if (hosts === 'config') {
|
|
@@ -587,7 +598,7 @@ export async function sendNpm(path, hosts) {
|
|
|
587
598
|
// --- 局域网模式 ---
|
|
588
599
|
const time = lTime.stamp();
|
|
589
600
|
/** --- 返回成功的 host --- */
|
|
590
|
-
const rtn =
|
|
601
|
+
const rtn = {};
|
|
591
602
|
for (const host of hosts) {
|
|
592
603
|
const res = await lUndici.get('http://' + host + ':' + globalConfig.rpcPort.toString() + '/' + lCrypto.aesEncrypt(lText.stringifyJson({
|
|
593
604
|
'action': 'npm',
|
|
@@ -602,9 +613,10 @@ export async function sendNpm(path, hosts) {
|
|
|
602
613
|
}
|
|
603
614
|
const str = content.toString();
|
|
604
615
|
if (str === 'Done') {
|
|
605
|
-
rtn
|
|
616
|
+
rtn[host] = { 'result': true, 'return': 'Done' };
|
|
606
617
|
}
|
|
607
618
|
else {
|
|
619
|
+
rtn[host] = { 'result': false, 'return': str };
|
|
608
620
|
debug('[CORE][sendNpmInstall] rpc server content error:', str);
|
|
609
621
|
}
|
|
610
622
|
}
|
|
@@ -626,7 +638,9 @@ export async function setGlobal(key, data, hosts) {
|
|
|
626
638
|
'key': key,
|
|
627
639
|
'data': data
|
|
628
640
|
});
|
|
629
|
-
return
|
|
641
|
+
return {
|
|
642
|
+
'127.0.0.1': { 'result': true, 'return': 'Done' }
|
|
643
|
+
};
|
|
630
644
|
}
|
|
631
645
|
if (hosts === 'config') {
|
|
632
646
|
hosts = globalConfig.hosts;
|
|
@@ -634,7 +648,7 @@ export async function setGlobal(key, data, hosts) {
|
|
|
634
648
|
// --- 局域网模式 ---
|
|
635
649
|
const time = lTime.stamp();
|
|
636
650
|
/** --- 返回成功的 host --- */
|
|
637
|
-
const rtn =
|
|
651
|
+
const rtn = {};
|
|
638
652
|
for (const host of hosts) {
|
|
639
653
|
const res = await lUndici.get('http://' + host + ':' + globalConfig.rpcPort.toString() + '/' + lCrypto.aesEncrypt(lText.stringifyJson({
|
|
640
654
|
'action': 'global',
|
|
@@ -648,7 +662,10 @@ export async function setGlobal(key, data, hosts) {
|
|
|
648
662
|
}
|
|
649
663
|
const str = content.toString();
|
|
650
664
|
if (str === 'Done') {
|
|
651
|
-
rtn
|
|
665
|
+
rtn[host] = { 'result': true, 'return': 'Done' };
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
rtn[host] = { 'result': false, 'return': str };
|
|
652
669
|
}
|
|
653
670
|
}
|
|
654
671
|
return rtn;
|
|
@@ -730,7 +747,7 @@ export async function sendProject(path, key, value, hosts) {
|
|
|
730
747
|
// --- 局域网模式 ---
|
|
731
748
|
const time = lTime.stamp();
|
|
732
749
|
/** --- 返回成功的 host --- */
|
|
733
|
-
const rtn =
|
|
750
|
+
const rtn = {};
|
|
734
751
|
for (const host of hosts) {
|
|
735
752
|
const res = await lUndici.get('http://' + host + ':' + globalConfig.rpcPort.toString() + '/' + lCrypto.aesEncrypt(lText.stringifyJson({
|
|
736
753
|
'action': 'project',
|
|
@@ -746,9 +763,10 @@ export async function sendProject(path, key, value, hosts) {
|
|
|
746
763
|
}
|
|
747
764
|
const str = content.toString();
|
|
748
765
|
if (str === 'Done') {
|
|
749
|
-
rtn
|
|
766
|
+
rtn[host] = { 'result': true, 'return': 'Done' };
|
|
750
767
|
}
|
|
751
768
|
else {
|
|
769
|
+
rtn[host] = { 'result': false, 'return': str };
|
|
752
770
|
debug('[CORE][sendProject] rpc server content error:', str);
|
|
753
771
|
}
|
|
754
772
|
}
|
|
@@ -770,7 +788,7 @@ export async function sendPackage(content, hosts) {
|
|
|
770
788
|
// --- 局域网模式 ---
|
|
771
789
|
const time = lTime.stamp();
|
|
772
790
|
/** --- 返回成功的 host --- */
|
|
773
|
-
const rtn =
|
|
791
|
+
const rtn = {};
|
|
774
792
|
for (const host of hosts) {
|
|
775
793
|
const res = await lUndici.get('http://' + host + ':' + globalConfig.rpcPort.toString() + '/' + lCrypto.aesEncrypt(lText.stringifyJson({
|
|
776
794
|
'action': 'package',
|
|
@@ -785,9 +803,10 @@ export async function sendPackage(content, hosts) {
|
|
|
785
803
|
}
|
|
786
804
|
const str = resContent.toString();
|
|
787
805
|
if (str === 'Done') {
|
|
788
|
-
rtn
|
|
806
|
+
rtn[host] = { 'result': true, 'return': 'Done' };
|
|
789
807
|
}
|
|
790
808
|
else {
|
|
809
|
+
rtn[host] = { 'result': false, 'return': str };
|
|
791
810
|
debug('[CORE][sendPackage] rpc server content error:', str);
|
|
792
811
|
}
|
|
793
812
|
}
|
package/lib/sql.d.ts
CHANGED
|
@@ -69,6 +69,16 @@ export declare class Sql {
|
|
|
69
69
|
* @param conflict 冲突字段,PostgreSQL 用于指定 ON CONFLICT 字段;MySQL 时忽略,因为会对所有唯一键冲突执行更新
|
|
70
70
|
*/
|
|
71
71
|
upsert(data: kebab.Json, conflict?: string | string[]): this;
|
|
72
|
+
/**
|
|
73
|
+
* --- 批量 UPDATE,以子查询作为数据源,纯更新语义(不会插入新行)---
|
|
74
|
+
* --- MySQL: UPDATE t INNER JOIN (SELECT col AS alias ... UNION ALL SELECT ...) AS tmp ON t.key=tmp.key SET t.c=tmp.c ---
|
|
75
|
+
* --- PostgreSQL: UPDATE t SET c=tmp.c FROM (VALUES ($1,...)) AS tmp(cols) WHERE t.key=tmp.key ---
|
|
76
|
+
* @param table 表名
|
|
77
|
+
* @param key 用于定位的主键/唯一键字段名
|
|
78
|
+
* @param cols 要更新的列名数组(不含 key)
|
|
79
|
+
* @param rows 数据行数组,每行顺序为 [keyVal, col1Val, col2Val, ...](与 [key, ...cols] 对应)
|
|
80
|
+
*/
|
|
81
|
+
updateByValues(table: string, key: string, cols: string[], rows: any[][]): this;
|
|
72
82
|
/**
|
|
73
83
|
* --- '*', 'xx' ---
|
|
74
84
|
* @param c 字段字符串或字段数组
|
|
@@ -237,6 +247,11 @@ export declare class Sql {
|
|
|
237
247
|
private _isValue;
|
|
238
248
|
/** --- 获取占位符 --- */
|
|
239
249
|
private _placeholder;
|
|
250
|
+
/**
|
|
251
|
+
* --- 返回 PostgreSQL VALUES 第一行的显式类型转换后缀,用于帮助 PostgreSQL 推断 VALUES 派生表列类型 ---
|
|
252
|
+
* @param v 要处理的值
|
|
253
|
+
*/
|
|
254
|
+
private _pgCastSuffix;
|
|
240
255
|
/**
|
|
241
256
|
* --- 处理单个值,检测数据类型并返回 SQL 和 data ---
|
|
242
257
|
* @param v 要处理的值
|
package/lib/sql.js
CHANGED
|
@@ -166,6 +166,59 @@ export class Sql {
|
|
|
166
166
|
}
|
|
167
167
|
return this;
|
|
168
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* --- 批量 UPDATE,以子查询作为数据源,纯更新语义(不会插入新行)---
|
|
171
|
+
* --- MySQL: UPDATE t INNER JOIN (SELECT col AS alias ... UNION ALL SELECT ...) AS tmp ON t.key=tmp.key SET t.c=tmp.c ---
|
|
172
|
+
* --- PostgreSQL: UPDATE t SET c=tmp.c FROM (VALUES ($1,...)) AS tmp(cols) WHERE t.key=tmp.key ---
|
|
173
|
+
* @param table 表名
|
|
174
|
+
* @param key 用于定位的主键/唯一键字段名
|
|
175
|
+
* @param cols 要更新的列名数组(不含 key)
|
|
176
|
+
* @param rows 数据行数组,每行顺序为 [keyVal, col1Val, col2Val, ...](与 [key, ...cols] 对应)
|
|
177
|
+
*/
|
|
178
|
+
updateByValues(table, key, cols, rows) {
|
|
179
|
+
this._data = [];
|
|
180
|
+
this._placeholderCounter = 1;
|
|
181
|
+
const allCols = [key, ...cols];
|
|
182
|
+
const quotedTable = this.field(table, this._pre);
|
|
183
|
+
const quotedKey = this.field(key);
|
|
184
|
+
if (this._service === ESERVICE.MYSQL) {
|
|
185
|
+
// --- MySQL 8.0.19+ VALUES ROW() 派生表语法 ---
|
|
186
|
+
const valueParts = [];
|
|
187
|
+
for (const row of rows) {
|
|
188
|
+
const parts = row.map(v => {
|
|
189
|
+
const result = this._processValue(v);
|
|
190
|
+
if (result.data.length > 0) {
|
|
191
|
+
this._data.push(...result.data);
|
|
192
|
+
}
|
|
193
|
+
return result.sql;
|
|
194
|
+
});
|
|
195
|
+
valueParts.push(`ROW(${parts.join(', ')})`);
|
|
196
|
+
}
|
|
197
|
+
const tmpCols = allCols.map(c => this.field(c)).join(', ');
|
|
198
|
+
const setClauses = cols.map(c => `t.${this.field(c)} = tmp.${this.field(c)}`).join(', ');
|
|
199
|
+
this._sql = [`UPDATE ${quotedTable} t INNER JOIN (VALUES ${valueParts.join(', ')}) AS tmp(${tmpCols}) ON t.${quotedKey} = tmp.${quotedKey} SET ${setClauses}`];
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// --- PostgreSQL 使用 UPDATE FROM (VALUES ...) ---
|
|
203
|
+
const valueParts = [];
|
|
204
|
+
for (let ri = 0; ri < rows.length; ri++) {
|
|
205
|
+
const row = rows[ri];
|
|
206
|
+
const parts = row.map(v => {
|
|
207
|
+
const result = this._processValue(v);
|
|
208
|
+
if (result.data.length > 0) {
|
|
209
|
+
this._data.push(...result.data);
|
|
210
|
+
}
|
|
211
|
+
// --- 第一行加显式类型转换,帮助 PostgreSQL 推断 VALUES 派生表的列类型 ---
|
|
212
|
+
return ri === 0 ? result.sql + this._pgCastSuffix(v) : result.sql;
|
|
213
|
+
});
|
|
214
|
+
valueParts.push(`(${parts.join(', ')})`);
|
|
215
|
+
}
|
|
216
|
+
const tmpCols = allCols.map(c => this.field(c)).join(', ');
|
|
217
|
+
const setClauses = cols.map(c => `${this.field(c)} = tmp.${this.field(c)}`).join(', ');
|
|
218
|
+
this._sql = [`UPDATE ${quotedTable} t SET ${setClauses} FROM (VALUES ${valueParts.join(', ')}) AS tmp(${tmpCols}) WHERE t.${quotedKey} = tmp.${quotedKey}`];
|
|
219
|
+
}
|
|
220
|
+
return this;
|
|
221
|
+
}
|
|
169
222
|
/**
|
|
170
223
|
* --- '*', 'xx' ---
|
|
171
224
|
* @param c 字段字符串或字段数组
|
|
@@ -984,6 +1037,48 @@ export class Sql {
|
|
|
984
1037
|
_placeholder() {
|
|
985
1038
|
return this._service === ESERVICE.MYSQL ? '?' : `$${this._placeholderCounter++}`;
|
|
986
1039
|
}
|
|
1040
|
+
/**
|
|
1041
|
+
* --- 返回 PostgreSQL VALUES 第一行的显式类型转换后缀,用于帮助 PostgreSQL 推断 VALUES 派生表列类型 ---
|
|
1042
|
+
* @param v 要处理的值
|
|
1043
|
+
*/
|
|
1044
|
+
_pgCastSuffix(v) {
|
|
1045
|
+
if (v === null || v === undefined) {
|
|
1046
|
+
return '';
|
|
1047
|
+
}
|
|
1048
|
+
if (typeof v === 'number') {
|
|
1049
|
+
return Number.isInteger(v) ? '::bigint' : '::float8';
|
|
1050
|
+
}
|
|
1051
|
+
if (typeof v === 'boolean') {
|
|
1052
|
+
return '::boolean';
|
|
1053
|
+
}
|
|
1054
|
+
if (v instanceof Buffer) {
|
|
1055
|
+
return '::bytea';
|
|
1056
|
+
}
|
|
1057
|
+
if (Array.isArray(v)) {
|
|
1058
|
+
// --- 函数式语法 ['FUNC(?)', [...]],不加转换 ---
|
|
1059
|
+
if (typeof v[0] === 'string' && v[0].includes('(')) {
|
|
1060
|
+
return '';
|
|
1061
|
+
}
|
|
1062
|
+
// --- POLYGON ---
|
|
1063
|
+
if (v[0]?.y !== undefined) {
|
|
1064
|
+
return '::polygon';
|
|
1065
|
+
}
|
|
1066
|
+
// --- JSON 数组或 PG 原生数组(text[]、int[] 等),
|
|
1067
|
+
// 不加转换,由 pg 驱动与目标列类型决定 ---
|
|
1068
|
+
return '';
|
|
1069
|
+
}
|
|
1070
|
+
if (typeof v === 'object') {
|
|
1071
|
+
// --- POINT ---
|
|
1072
|
+
if (v.y !== undefined) {
|
|
1073
|
+
return '::point';
|
|
1074
|
+
}
|
|
1075
|
+
// --- JSON 对象(用户应通过 sql.json() 包裹为字符串后传入),不加转换 ---
|
|
1076
|
+
return '';
|
|
1077
|
+
}
|
|
1078
|
+
// --- string:保持 unknown 类型,兼容 text/varchar/jsonb 等目标列类型;
|
|
1079
|
+
// 使用 sql.json() 包裹的 jsonb 数据经此路径,unknown 可隐式 cast 到 jsonb ---
|
|
1080
|
+
return '';
|
|
1081
|
+
}
|
|
987
1082
|
/**
|
|
988
1083
|
* --- 处理单个值,检测数据类型并返回 SQL 和 data ---
|
|
989
1084
|
* @param v 要处理的值
|
package/package.json
CHANGED
package/sys/master.js
CHANGED
|
@@ -404,7 +404,7 @@ function createRpcListener() {
|
|
|
404
404
|
// --- 特殊文件不能覆盖 ---
|
|
405
405
|
continue;
|
|
406
406
|
}
|
|
407
|
-
if (fname.endsWith('.js.map') || fname.endsWith('.ts') || fname.endsWith('.scss') || fname.endsWith('.gitignore') || fname.endsWith('.DS_Store')) {
|
|
407
|
+
if (fname.endsWith('.js.map') || fname.endsWith('.ts') || fname.endsWith('.tsx') || fname.endsWith('.scss') || fname.endsWith('.gitignore') || fname.endsWith('.DS_Store')) {
|
|
408
408
|
// --- 测试或开发文件不覆盖 ---
|
|
409
409
|
continue;
|
|
410
410
|
}
|
package/sys/mod.d.ts
CHANGED
|
@@ -180,8 +180,10 @@ export default class Mod {
|
|
|
180
180
|
/**
|
|
181
181
|
* --- 批量更新数据 ---
|
|
182
182
|
* @param db 数据库对象
|
|
183
|
-
* @param data
|
|
184
|
-
*
|
|
183
|
+
* @param data 数据列表,每个元素必须包含 key 字段,其余字段为要更新的列;
|
|
184
|
+
* 支持稀疏数据(不同元素可以拥有不同的列集合),内部会自动按列集合分组批量执行
|
|
185
|
+
* @param key 用于定位记录的字段名(主键或唯一键),该字段仅用于 WHERE 条件匹配,不会被更新;
|
|
186
|
+
* data 中每个元素都必须包含此字段,否则该元素会被跳过
|
|
185
187
|
* @param opt 选项(opt.pre: MySQL 表前缀/PostgreSQL Schema 名)
|
|
186
188
|
*/
|
|
187
189
|
static updateList(db: lDb.Pool | lDb.Transaction, data: Array<Record<string, any>>, key: string, opt?: {
|
package/sys/mod.js
CHANGED
|
@@ -305,74 +305,53 @@ export default class Mod {
|
|
|
305
305
|
/**
|
|
306
306
|
* --- 批量更新数据 ---
|
|
307
307
|
* @param db 数据库对象
|
|
308
|
-
* @param data
|
|
309
|
-
*
|
|
308
|
+
* @param data 数据列表,每个元素必须包含 key 字段,其余字段为要更新的列;
|
|
309
|
+
* 支持稀疏数据(不同元素可以拥有不同的列集合),内部会自动按列集合分组批量执行
|
|
310
|
+
* @param key 用于定位记录的字段名(主键或唯一键),该字段仅用于 WHERE 条件匹配,不会被更新;
|
|
311
|
+
* data 中每个元素都必须包含此字段,否则该元素会被跳过
|
|
310
312
|
* @param opt 选项(opt.pre: MySQL 表前缀/PostgreSQL Schema 名)
|
|
311
313
|
*/
|
|
312
314
|
static async updateList(db, data, key, opt = {}) {
|
|
313
315
|
if (!data.length) {
|
|
314
316
|
return true;
|
|
315
317
|
}
|
|
316
|
-
// ---
|
|
317
|
-
const
|
|
318
|
+
// --- 按列集合分组(处理稀疏数据,保证每组内所有行的列完全一致)---
|
|
319
|
+
const groups = new Map();
|
|
318
320
|
for (const item of data) {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
321
|
+
const itemCols = Object.keys(item).filter(k => k !== key).sort();
|
|
322
|
+
if (itemCols.length === 0) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const groupKey = itemCols.join('\0');
|
|
326
|
+
if (!groups.has(groupKey)) {
|
|
327
|
+
groups.set(groupKey, []);
|
|
323
328
|
}
|
|
329
|
+
groups.get(groupKey).push(item);
|
|
324
330
|
}
|
|
325
|
-
if (
|
|
331
|
+
if (groups.size === 0) {
|
|
326
332
|
return true;
|
|
327
333
|
}
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
let caseSql = `(CASE ${sq.field(key)}`;
|
|
348
|
-
const params = [];
|
|
349
|
-
let hasUpdate = false;
|
|
350
|
-
for (const item of batch) {
|
|
351
|
-
if (item[col] !== undefined) {
|
|
352
|
-
caseSql += ` WHEN ? THEN ?`;
|
|
353
|
-
params.push(item[key], item[col]);
|
|
354
|
-
hasUpdate = true;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
if (hasUpdate) {
|
|
358
|
-
caseSql += ` ELSE ${sq.field(col)} END)`;
|
|
359
|
-
updates[col] = [caseSql, params];
|
|
334
|
+
const tableName = this._$table + (opt.index ? ('_' + opt.index) : '');
|
|
335
|
+
for (const [, groupItems] of groups) {
|
|
336
|
+
const cols = Object.keys(groupItems[0]).filter(k => k !== key).sort();
|
|
337
|
+
const allCols = [key, ...cols];
|
|
338
|
+
// --- 每行占位符数量 = key + 所有列,分批避免超出数据库参数上限 ---
|
|
339
|
+
const batchSize = Math.floor(60000 / allCols.length);
|
|
340
|
+
for (let i = 0; i < groupItems.length; i += batchSize) {
|
|
341
|
+
const batch = groupItems.slice(i, i + batchSize);
|
|
342
|
+
const sq = lSql.get({
|
|
343
|
+
'service': db.getService() ?? lDb.ESERVICE.PGSQL,
|
|
344
|
+
'ctr': opt.ctr,
|
|
345
|
+
'pre': opt.pre ?? this._$pre,
|
|
346
|
+
});
|
|
347
|
+
const rows = batch.map(item => allCols.map(c => item[c] ?? null));
|
|
348
|
+
sq.updateByValues(tableName, key, cols, rows);
|
|
349
|
+
const r = await db.execute(sq.getSql(), sq.getData());
|
|
350
|
+
if (r.packet === null) {
|
|
351
|
+
lCore.log(opt.ctr ?? {}, '[MOD][updateList] ' + (lText.stringifyJson(r.error?.message ?? '').slice(1, -1) + ' - ' + sq.format()).replaceAll('"', '""'), '-error');
|
|
352
|
+
return false;
|
|
360
353
|
}
|
|
361
354
|
}
|
|
362
|
-
// --- 收集 keys ---
|
|
363
|
-
for (const item of batch) {
|
|
364
|
-
keys.push(item[key]);
|
|
365
|
-
}
|
|
366
|
-
if (Object.keys(updates).length === 0) {
|
|
367
|
-
continue;
|
|
368
|
-
}
|
|
369
|
-
sq.update(this._$table + (opt.index ? ('_' + opt.index) : ''), updates)
|
|
370
|
-
.where({ [key]: keys });
|
|
371
|
-
const r = await db.execute(sq.getSql(), sq.getData());
|
|
372
|
-
if (r.packet === null) {
|
|
373
|
-
lCore.log(opt.ctr ?? {}, '[MOD][updateList] ' + (lText.stringifyJson(r.error?.message ?? '').slice(1, -1) + ' - ' + sq.format()).replaceAll('"', '""'), '-error');
|
|
374
|
-
return false;
|
|
375
|
-
}
|
|
376
355
|
}
|
|
377
356
|
return true;
|
|
378
357
|
}
|
package/www/example/ctr/test.js
CHANGED
|
@@ -1407,7 +1407,10 @@ for (let i = 0; i < 30000; ++i) {
|
|
|
1407
1407
|
else {
|
|
1408
1408
|
// --- jsonl 格式:object ---
|
|
1409
1409
|
const r = row;
|
|
1410
|
-
for (const val of [
|
|
1410
|
+
for (const val of [
|
|
1411
|
+
r.time, r.unix, r.url,
|
|
1412
|
+
r.cookie, r.session, r.userAgent, r.realIp, r.cfIp, r.xIp, r.osMem, r.procMem, r.message
|
|
1413
|
+
]) {
|
|
1411
1414
|
echo.push('<td>' + lText.htmlescape(typeof val === 'string' ? val : lText.stringifyJson(val) ?? '') + '</td>');
|
|
1412
1415
|
}
|
|
1413
1416
|
}
|
|
@@ -1466,12 +1469,12 @@ to: ${to}`
|
|
|
1466
1469
|
return echo.join('') + '<br><br>' + this._getEnd();
|
|
1467
1470
|
}
|
|
1468
1471
|
async coreReload() {
|
|
1469
|
-
await lCore.sendReload();
|
|
1470
|
-
return
|
|
1472
|
+
const list = await lCore.sendReload();
|
|
1473
|
+
return `The reload request has been sent, please review the console.<br>Hosts: ${JSON.stringify(list)}<br><br>` + this._getEnd();
|
|
1471
1474
|
}
|
|
1472
1475
|
async coreRestart() {
|
|
1473
|
-
await lCore.sendRestart();
|
|
1474
|
-
return
|
|
1476
|
+
const list = await lCore.sendRestart();
|
|
1477
|
+
return `The restart request has been sent, please review the console.<br>Hosts: ${JSON.stringify(list)}<br><br>` + this._getEnd();
|
|
1475
1478
|
}
|
|
1476
1479
|
async corePm2() {
|
|
1477
1480
|
const name = this._get['name'] ?? '';
|
|
@@ -1483,7 +1486,7 @@ to: ${to}`
|
|
|
1483
1486
|
return 'Invalid action. Must be: start, stop, restart<br><br>' + this._getEnd();
|
|
1484
1487
|
}
|
|
1485
1488
|
const list = await lCore.sendPm2(name, action);
|
|
1486
|
-
return `PM2 ${action} request has been sent for "${name}".<br>
|
|
1489
|
+
return `PM2 ${action} request has been sent for "${name}".<br>Hosts: ${JSON.stringify(list)}<br><br>` + this._getEnd();
|
|
1487
1490
|
}
|
|
1488
1491
|
async coreNpm() {
|
|
1489
1492
|
// --- 创建一个临时目录 ---
|
|
@@ -1498,7 +1501,7 @@ to: ${to}`
|
|
|
1498
1501
|
}
|
|
1499
1502
|
}));
|
|
1500
1503
|
const list = await lCore.sendNpm(path);
|
|
1501
|
-
return `NPM request has been sent.<br>
|
|
1504
|
+
return `NPM request has been sent.<br>Hosts: ${JSON.stringify(list)}<br><br>` + this._getEnd();
|
|
1502
1505
|
}
|
|
1503
1506
|
async coreGlobal() {
|
|
1504
1507
|
const ts = lTime.stamp().toString();
|