biz-a-cli 2.3.80-15235 → 2.3.80-15267
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/bin/deployEvent.js +1 -1
- package/bin/directHubEvent.js +27 -3
- package/bin/hub.js +3 -0
- package/bin/hubEvent.js +39 -1
- package/db/db.js +9 -5
- package/db/ds.js +14 -0
- package/engine/bpm/workflow-runtime.js +118 -15
- package/engine/bpm/workflow.js +54 -3
- package/engine/domain/appConfig.js +213 -0
- package/engine/domain/firebird-ddl.js +39 -0
- package/message-broker/clientEmitter.js +288 -0
- package/message-broker/dispatcher.js +22 -13
- package/package.json +1 -1
- package/scheduler/datalib.js +18 -0
- package/worker/cliScriptWorker.js +27 -6
- package/worker/cliWorkerPool.js +4 -1
package/bin/deployEvent.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
2
|
import { prepareScript, encryptScript } from './script.js';
|
|
3
3
|
import { IDLE_SOCKET_TIMEOUT_MILLISECONDS } from './hubEvent.js';
|
|
4
|
-
import { CLIENT_ROOM } from '
|
|
4
|
+
import { CLIENT_ROOM } from '../message-broker/clientEmitter.js';
|
|
5
5
|
|
|
6
6
|
export async function getAppData(apiOpts){
|
|
7
7
|
return await axios.request({
|
package/bin/directHubEvent.js
CHANGED
|
@@ -2,8 +2,8 @@ import { clientListener, RECONNECT_SOCKET_DELAY } from "./hubEvent.js";
|
|
|
2
2
|
import { deploymentListenerForVSCode } from "./deployEvent.js";
|
|
3
3
|
import { Tunnel as QuickTunnel } from "cloudflared";
|
|
4
4
|
import { Server as ioServer } from "socket.io";
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import { CLIENT_ROOM } from "../message-broker/clientEmitter.js";
|
|
6
|
+
import { ClientEmitter } from "../message-broker/clientEmitter.js";
|
|
7
7
|
|
|
8
8
|
export async function localhostTunnel(port, notifier) {
|
|
9
9
|
let tunnelUrl = "";
|
|
@@ -69,6 +69,26 @@ export function directHubEvent(serverSocket, argv) {
|
|
|
69
69
|
setConnectListeners(clientSocket);
|
|
70
70
|
clientListener(clientSocket, argv);
|
|
71
71
|
clientSocket.join(CLIENT_ROOM);
|
|
72
|
+
|
|
73
|
+
// isolated Client in a room using clientInstanceId
|
|
74
|
+
const clientInstanceId =
|
|
75
|
+
clientSocket.handshake.query.clientInstanceId;
|
|
76
|
+
if (clientInstanceId) {
|
|
77
|
+
clientSocket.join(clientInstanceId);
|
|
78
|
+
clientSocket.on("ESB_ACK", (msgId) => {
|
|
79
|
+
ClientEmitter.acknowledgeMessage(msgId, clientInstanceId);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// isolated Client in a room using argv.subdomain and UserId
|
|
84
|
+
const userId = clientSocket.handshake.query.userId;
|
|
85
|
+
if (
|
|
86
|
+
userId !== undefined &&
|
|
87
|
+
userId !== null &&
|
|
88
|
+
String(userId) !== "-1"
|
|
89
|
+
) {
|
|
90
|
+
clientSocket.join(`${argv.subdomain}_USER_${userId}`);
|
|
91
|
+
}
|
|
72
92
|
} else if (clientSocket.handshake.query.isDeploy) {
|
|
73
93
|
setConnectListeners(clientSocket);
|
|
74
94
|
deploymentListenerForVSCode(clientSocket, argv);
|
|
@@ -89,7 +109,11 @@ export function createSocketServer(httpServer, cliIpAddress = "127.0.0.1") {
|
|
|
89
109
|
/\.vscode-cdn\.net$/,
|
|
90
110
|
].concat(
|
|
91
111
|
process.env.NODE_ENV === "production"
|
|
92
|
-
? [
|
|
112
|
+
? [
|
|
113
|
+
`http://${cliIpAddress}:4200`,
|
|
114
|
+
"http://localhost:4200",
|
|
115
|
+
"http://localhost",
|
|
116
|
+
]
|
|
93
117
|
: [`http://${cliIpAddress}:4200`, "http://localhost:4200"],
|
|
94
118
|
),
|
|
95
119
|
},
|
package/bin/hub.js
CHANGED
|
@@ -24,6 +24,7 @@ import { setGlobalConfig } from "../db/ds.js";
|
|
|
24
24
|
import { CLI_Agent } from "../message-broker/agent.js";
|
|
25
25
|
import { initBpmAgent } from "../engine/bpm/bpm-agent.js";
|
|
26
26
|
import { bootMicroservices } from "../message-broker/orchestrator.js";
|
|
27
|
+
import { ClientEmitter } from "../message-broker/clientEmitter.js";
|
|
27
28
|
|
|
28
29
|
const logger = createLogger({
|
|
29
30
|
level: "info",
|
|
@@ -294,6 +295,8 @@ if (argv.mode.trim().toLowerCase() === "agent") {
|
|
|
294
295
|
// directHubEvent(createSocketServer(server, argv.cliAddress().ip), argv);
|
|
295
296
|
directHubEvent(ioServerInstance, argv);
|
|
296
297
|
|
|
298
|
+
ClientEmitter.init(ioServerInstance, socket, argv.subdomain);
|
|
299
|
+
|
|
297
300
|
bootMicroservices(argv, ioServerInstance);
|
|
298
301
|
|
|
299
302
|
if (argv.publish == true) {
|
package/bin/hubEvent.js
CHANGED
|
@@ -10,6 +10,7 @@ const packageJson = require("../package.json");
|
|
|
10
10
|
// import { pipeline } from 'node:stream'
|
|
11
11
|
import { deploymentListenerForHubServer } from "./deployEvent.js";
|
|
12
12
|
import { execCLIScriptWorker, workerStats } from "../worker/cliWorkerPool.js";
|
|
13
|
+
import { ClientEmitter } from "../message-broker/clientEmitter.js";
|
|
13
14
|
|
|
14
15
|
export const IDLE_SOCKET_TIMEOUT_MILLISECONDS = 1000 * 28;
|
|
15
16
|
export const RECONNECT_SOCKET_DELAY = 60 * 1000;
|
|
@@ -227,6 +228,12 @@ export const hubEvent = (socket, argv, notifier) => {
|
|
|
227
228
|
});
|
|
228
229
|
clientListener(socket, argv);
|
|
229
230
|
deploymentListenerForHubServer(socket, argv);
|
|
231
|
+
|
|
232
|
+
// ESB client emitter. Catch the forwarded ACK from the Hub Server
|
|
233
|
+
socket.on("ESB_ACK", ({ msgId, clientInstanceId }) => {
|
|
234
|
+
ClientEmitter.acknowledgeMessage(msgId, clientInstanceId);
|
|
235
|
+
});
|
|
236
|
+
|
|
230
237
|
return socket;
|
|
231
238
|
};
|
|
232
239
|
|
|
@@ -298,7 +305,8 @@ const EngineRegistry = {
|
|
|
298
305
|
"bpm/workflowRT": () => import("../engine/bpm/workflow-runtime.js"),
|
|
299
306
|
"domain/system": () => import("../engine/domain/system.js"),
|
|
300
307
|
"domain/appConfig": () => import("../engine/domain/appConfig.js"),
|
|
301
|
-
"domain/applicationConfig": () =>
|
|
308
|
+
"domain/applicationConfig": () =>
|
|
309
|
+
import("../engine/domain/applicationConfig.js"),
|
|
302
310
|
};
|
|
303
311
|
|
|
304
312
|
const handleCliCommand = async (data, cb, argv) => {
|
|
@@ -357,12 +365,42 @@ const handleCliCommand = async (data, cb, argv) => {
|
|
|
357
365
|
cb(null, { success: true, data: result });
|
|
358
366
|
break;
|
|
359
367
|
}
|
|
368
|
+
case "emitter": {
|
|
369
|
+
const requestData = data.data || {};
|
|
370
|
+
const { action, userIds, groupIds, eventName, payload } =
|
|
371
|
+
requestData;
|
|
372
|
+
|
|
373
|
+
if (action === "emitUsers" && Array.isArray(userIds)) {
|
|
374
|
+
ClientEmitter.emitUsers(userIds, eventName, payload).catch(
|
|
375
|
+
(err) => console.error("[ESB] emitUsers Error:", err),
|
|
376
|
+
);
|
|
377
|
+
} else if (
|
|
378
|
+
action === "emitUserGroups" &&
|
|
379
|
+
Array.isArray(groupIds)
|
|
380
|
+
) {
|
|
381
|
+
ClientEmitter.emitUserGroups(
|
|
382
|
+
groupIds,
|
|
383
|
+
eventName,
|
|
384
|
+
payload,
|
|
385
|
+
).catch((err) =>
|
|
386
|
+
console.error("[ESB] emitUserGroups Error:", err),
|
|
387
|
+
);
|
|
388
|
+
} else if (action === "broadcastClient") {
|
|
389
|
+
ClientEmitter.broadcastClient(eventName, payload);
|
|
390
|
+
}
|
|
360
391
|
|
|
392
|
+
cb(null, {
|
|
393
|
+
success: true,
|
|
394
|
+
message: `Handed over ${action} to ESB ClientEmitter`,
|
|
395
|
+
});
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
361
398
|
default:
|
|
362
399
|
cb(`Unknown CLI command '${command}'`, null);
|
|
363
400
|
}
|
|
364
401
|
} catch (err) {
|
|
365
402
|
cb(err.message || err, null);
|
|
403
|
+
// console.error("[CLICommand] Error:", err);
|
|
366
404
|
}
|
|
367
405
|
};
|
|
368
406
|
|
package/db/db.js
CHANGED
|
@@ -17,7 +17,11 @@ export const save = async (payload, config = {}) => {
|
|
|
17
17
|
return await adapter.save(payload, config);
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
export const
|
|
20
|
+
export const remove = async (payload, config = {}) => {
|
|
21
|
+
return await adapter.remove(payload, config);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const removeMD = async (id, masterTable, detailTables, config = {}) => {
|
|
21
25
|
const payload = {
|
|
22
26
|
id: id,
|
|
23
27
|
master: masterTable,
|
|
@@ -26,10 +30,6 @@ export const deleteMD = async (id, masterTable, detailTables, config = {}) => {
|
|
|
26
30
|
return await adapter.removeMD(payload, config);
|
|
27
31
|
};
|
|
28
32
|
|
|
29
|
-
export const remove = async (payload, config = {}) => {
|
|
30
|
-
return await adapter.remove(payload, config);
|
|
31
|
-
};
|
|
32
|
-
|
|
33
33
|
export const list = async (payload, config = {}) => {
|
|
34
34
|
return await adapter.list(payload, config);
|
|
35
35
|
};
|
|
@@ -37,3 +37,7 @@ export const list = async (payload, config = {}) => {
|
|
|
37
37
|
export const executeBlock = async (sqlString, config = {}) => {
|
|
38
38
|
return await adapter.executeBlock(sqlString, config);
|
|
39
39
|
};
|
|
40
|
+
|
|
41
|
+
export const ormGet = async (payload, config = {}) => {
|
|
42
|
+
return await adapter.ormGet(payload, config);
|
|
43
|
+
};
|
package/db/ds.js
CHANGED
|
@@ -269,3 +269,17 @@ export const executeBlock = async (sqlString, config = {}) => {
|
|
|
269
269
|
const data = buildDataSnapPayload(cfg, "block", flatSql);
|
|
270
270
|
return await dsReq(url, data, cfg.timeout);
|
|
271
271
|
};
|
|
272
|
+
|
|
273
|
+
export const ormGet = async (payload, config = {}) => {
|
|
274
|
+
const cfg = resolveConfig(config);
|
|
275
|
+
const url = buildDataSnapUrl(cfg, "orm");
|
|
276
|
+
const data = buildDataSnapPayload(cfg, "get", payload);
|
|
277
|
+
|
|
278
|
+
const res = await dsReq(url, data, cfg.timeout);
|
|
279
|
+
|
|
280
|
+
if (res && res.data) {
|
|
281
|
+
return typeof res.data === "string" ? JSON.parse(res.data) : res.data;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return res;
|
|
285
|
+
};
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
queryData,
|
|
3
3
|
save as dbSave,
|
|
4
|
-
|
|
4
|
+
removeMD,
|
|
5
5
|
getGlobalConfig,
|
|
6
|
+
ormGet,
|
|
6
7
|
} from "../../db/db.js";
|
|
7
8
|
import vm from "node:vm";
|
|
8
9
|
import { execCLIScriptWorker } from "../../worker/cliWorkerPool.js";
|
|
9
10
|
import axios from "axios";
|
|
11
|
+
import { ClientEmitter } from "../../message-broker/clientEmitter.js";
|
|
10
12
|
|
|
11
13
|
// ================
|
|
12
14
|
// INTERNAL HELPERS
|
|
@@ -98,6 +100,42 @@ async function evaluateGuardRule(guardType, guardRuleKey, entityData) {
|
|
|
98
100
|
}
|
|
99
101
|
}
|
|
100
102
|
|
|
103
|
+
async function processInstanceUpsertResult(
|
|
104
|
+
requesterPayload,
|
|
105
|
+
apiConfig,
|
|
106
|
+
result,
|
|
107
|
+
fallbackId,
|
|
108
|
+
) {
|
|
109
|
+
if (result && result.success == "OK") {
|
|
110
|
+
try {
|
|
111
|
+
const savedId = result.data?.[0]?.ID || fallbackId;
|
|
112
|
+
if (savedId) {
|
|
113
|
+
const newRecord = await ormGet(
|
|
114
|
+
{
|
|
115
|
+
SYS$BPM_WF_INSTANCE: {
|
|
116
|
+
ID: savedId,
|
|
117
|
+
"*": "",
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
apiConfig,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
ClientEmitter.broadcastClient(
|
|
124
|
+
"SYS$BPM_WF_INSTANCE_UPSERT",
|
|
125
|
+
newRecord,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
ClientEmitter.notifyRequester(requesterPayload, newRecord);
|
|
129
|
+
}
|
|
130
|
+
} catch (esbError) {
|
|
131
|
+
console.error(
|
|
132
|
+
"[ESB Error] Failed to broadcast instance upsert :",
|
|
133
|
+
esbError,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
101
139
|
// ===============
|
|
102
140
|
// ENGINE ENDPOINT
|
|
103
141
|
// ===============
|
|
@@ -246,6 +284,9 @@ export async function initiate(payload) {
|
|
|
246
284
|
{ SYS$BPM_WF_INSTANCE: instanceModel },
|
|
247
285
|
apiConfig,
|
|
248
286
|
);
|
|
287
|
+
|
|
288
|
+
await processInstanceUpsertResult(payload, apiConfig, result, null);
|
|
289
|
+
|
|
249
290
|
return result.data;
|
|
250
291
|
}
|
|
251
292
|
|
|
@@ -389,6 +430,7 @@ export async function executeTransition(payload) {
|
|
|
389
430
|
entityData = {},
|
|
390
431
|
apiConfig,
|
|
391
432
|
userId,
|
|
433
|
+
effectData = {},
|
|
392
434
|
} = payload;
|
|
393
435
|
|
|
394
436
|
const instParam = {
|
|
@@ -614,19 +656,49 @@ export async function executeTransition(payload) {
|
|
|
614
656
|
};
|
|
615
657
|
|
|
616
658
|
if (effects && effects.length > 0) {
|
|
617
|
-
transitionPayload.SYS$BPM_EFFECT_LOG = effects.map((eff) =>
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
659
|
+
transitionPayload.SYS$BPM_EFFECT_LOG = effects.map((eff) => {
|
|
660
|
+
let mergedPayload = {};
|
|
661
|
+
|
|
662
|
+
// 1. Parse the static template from the Workflow Definition
|
|
663
|
+
if (eff.payload_template) {
|
|
664
|
+
try {
|
|
665
|
+
mergedPayload = JSON.parse(eff.payload_template);
|
|
666
|
+
} catch (e) {}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// 2. Merge the dynamic context injected by the Biz-A Client (e.g., clientInstanceId)
|
|
670
|
+
if (effectData && Object.keys(effectData).length > 0) {
|
|
671
|
+
mergedPayload = { ...mergedPayload, ...effectData };
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// 3. Stringify the result, or fall back to null if completely empty
|
|
675
|
+
const finalPayloadString =
|
|
676
|
+
Object.keys(mergedPayload).length > 0
|
|
677
|
+
? JSON.stringify(mergedPayload)
|
|
678
|
+
: null;
|
|
679
|
+
|
|
680
|
+
return {
|
|
681
|
+
id: null,
|
|
682
|
+
sys$bpm_wf_instance_id: Number(instanceId),
|
|
683
|
+
sys$bpm_trans_def_id: transition.id,
|
|
684
|
+
effect_type: eff.effect_type,
|
|
685
|
+
event_name: eff.event_name,
|
|
686
|
+
payload_evaluated: finalPayloadString || null,
|
|
687
|
+
status: "PENDING",
|
|
688
|
+
retry_count: 0,
|
|
689
|
+
};
|
|
690
|
+
});
|
|
627
691
|
}
|
|
628
692
|
|
|
629
693
|
const result = await dbSave(transitionPayload, apiConfig);
|
|
694
|
+
|
|
695
|
+
await processInstanceUpsertResult(
|
|
696
|
+
payload,
|
|
697
|
+
apiConfig,
|
|
698
|
+
result,
|
|
699
|
+
Number(instanceId),
|
|
700
|
+
);
|
|
701
|
+
|
|
630
702
|
return result.data;
|
|
631
703
|
}
|
|
632
704
|
|
|
@@ -723,6 +795,14 @@ export async function terminate(payload) {
|
|
|
723
795
|
};
|
|
724
796
|
|
|
725
797
|
const result = await dbSave(ormPayload, apiConfig);
|
|
798
|
+
|
|
799
|
+
await processInstanceUpsertResult(
|
|
800
|
+
payload,
|
|
801
|
+
apiConfig,
|
|
802
|
+
result,
|
|
803
|
+
Number(instanceId),
|
|
804
|
+
);
|
|
805
|
+
|
|
726
806
|
return result.data;
|
|
727
807
|
}
|
|
728
808
|
|
|
@@ -735,13 +815,27 @@ export async function remove(payload) {
|
|
|
735
815
|
"SYS$BPM_EFFECT_LOG",
|
|
736
816
|
];
|
|
737
817
|
|
|
738
|
-
const result = await
|
|
818
|
+
const result = await removeMD(
|
|
739
819
|
Number(instanceId),
|
|
740
820
|
"SYS$BPM_WF_INSTANCE",
|
|
741
821
|
detailTables,
|
|
742
822
|
apiConfig,
|
|
743
823
|
);
|
|
744
|
-
|
|
824
|
+
|
|
825
|
+
if (result && result.success == "OK") {
|
|
826
|
+
try {
|
|
827
|
+
const data = [{ ID: instanceId }];
|
|
828
|
+
ClientEmitter.broadcastClient("SYS$BPM_WF_INSTANCE_REMOVE", data);
|
|
829
|
+
ClientEmitter.notifyRequester(payload, data);
|
|
830
|
+
} catch (esbError) {
|
|
831
|
+
console.error(
|
|
832
|
+
"[ESB Error] Failed to broadcast instance deletion:",
|
|
833
|
+
esbError,
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return result;
|
|
745
839
|
}
|
|
746
840
|
|
|
747
841
|
export async function myTasks(payload) {
|
|
@@ -1233,7 +1327,9 @@ export async function executeEffect(effectData) {
|
|
|
1233
1327
|
// instance when the effect carries no payloadTemplate.
|
|
1234
1328
|
const scriptData = Object.assign(
|
|
1235
1329
|
{},
|
|
1236
|
-
parsedPayload &&
|
|
1330
|
+
parsedPayload &&
|
|
1331
|
+
typeof parsedPayload === "object" &&
|
|
1332
|
+
!Array.isArray(parsedPayload)
|
|
1237
1333
|
? parsedPayload
|
|
1238
1334
|
: {},
|
|
1239
1335
|
{
|
|
@@ -1242,10 +1338,17 @@ export async function executeEffect(effectData) {
|
|
|
1242
1338
|
effectData.SYS$BPM_WF_INSTANCE_ID,
|
|
1243
1339
|
},
|
|
1244
1340
|
);
|
|
1341
|
+
|
|
1245
1342
|
const scriptResult = await execCLIScriptWorker(
|
|
1246
1343
|
apiConfig,
|
|
1247
1344
|
eventName,
|
|
1248
|
-
scriptData,
|
|
1345
|
+
// scriptData,
|
|
1346
|
+
{
|
|
1347
|
+
// to be same format as callback, live trigger and watcher, script data must be inside "body"
|
|
1348
|
+
body: scriptData,
|
|
1349
|
+
// You can even add mock headers if future scripts expect them
|
|
1350
|
+
headers: { "user-agent": "biz-a-bpm-engine" },
|
|
1351
|
+
},
|
|
1249
1352
|
);
|
|
1250
1353
|
|
|
1251
1354
|
// --- if cliScript of Effect need to run executeTransition ---
|
package/engine/bpm/workflow.js
CHANGED
|
@@ -1,4 +1,36 @@
|
|
|
1
|
-
import { save as dbSave, queryData,
|
|
1
|
+
import { save as dbSave, queryData, removeMD, ormGet } from "../../db/db.js";
|
|
2
|
+
import { ClientEmitter } from "../../message-broker/clientEmitter.js";
|
|
3
|
+
|
|
4
|
+
async function processUpsertResult(requesterPayload, apiConfig, result) {
|
|
5
|
+
if (result.success == "OK") {
|
|
6
|
+
try {
|
|
7
|
+
const savedId = result.data[0]?.ID || id;
|
|
8
|
+
if (savedId) {
|
|
9
|
+
const newRecord = await ormGet(
|
|
10
|
+
{
|
|
11
|
+
SYS$BPM_WORKFLOW_DEF: {
|
|
12
|
+
ID: savedId,
|
|
13
|
+
"*": "",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
apiConfig,
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
ClientEmitter.broadcastClient(
|
|
20
|
+
"SYS$BPM_WORKFLOW_DEF_UPSERT",
|
|
21
|
+
newRecord,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
ClientEmitter.notifyRequester(requesterPayload, newRecord);
|
|
25
|
+
}
|
|
26
|
+
} catch (esbError) {
|
|
27
|
+
console.error(
|
|
28
|
+
"[ESB Error] Failed to broadcast workflow upsert :",
|
|
29
|
+
esbError,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
2
34
|
|
|
3
35
|
export async function save(payload) {
|
|
4
36
|
const { id, rawJsonString, apiConfig, userId } = payload;
|
|
@@ -48,6 +80,9 @@ export async function save(payload) {
|
|
|
48
80
|
},
|
|
49
81
|
apiConfig,
|
|
50
82
|
);
|
|
83
|
+
|
|
84
|
+
await processUpsertResult(payload, apiConfig, result);
|
|
85
|
+
|
|
51
86
|
return result.data;
|
|
52
87
|
}
|
|
53
88
|
|
|
@@ -175,6 +210,9 @@ export async function publish(payload) {
|
|
|
175
210
|
{ SYS$BPM_WORKFLOW_DEF: workflowModel },
|
|
176
211
|
apiConfig,
|
|
177
212
|
);
|
|
213
|
+
|
|
214
|
+
await processUpsertResult(payload, apiConfig, result);
|
|
215
|
+
|
|
178
216
|
return result.data;
|
|
179
217
|
}
|
|
180
218
|
|
|
@@ -221,12 +259,25 @@ export async function remove(payload) {
|
|
|
221
259
|
},
|
|
222
260
|
];
|
|
223
261
|
|
|
224
|
-
const result = await
|
|
262
|
+
const result = await removeMD(
|
|
225
263
|
Number(id),
|
|
226
264
|
"SYS$BPM_WORKFLOW_DEF",
|
|
227
265
|
detailTables,
|
|
228
266
|
apiConfig,
|
|
229
267
|
);
|
|
230
268
|
|
|
231
|
-
|
|
269
|
+
if (result.success == "OK") {
|
|
270
|
+
try {
|
|
271
|
+
const data = [{ ID: id }];
|
|
272
|
+
ClientEmitter.broadcastClient("SYS$BPM_WORKFLOW_DEF_REMOVE", data);
|
|
273
|
+
ClientEmitter.notifyRequester(payload, data);
|
|
274
|
+
} catch (esbError) {
|
|
275
|
+
console.error(
|
|
276
|
+
"[ESB Error] Failed to broadcast workflow deletion:",
|
|
277
|
+
esbError,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return result;
|
|
232
283
|
}
|
|
@@ -852,6 +852,93 @@ export const buildTableSyncPlan = (expected, actual, options) => {
|
|
|
852
852
|
};
|
|
853
853
|
};
|
|
854
854
|
|
|
855
|
+
const normalizeScopedDropIdentifier = (value, label) => {
|
|
856
|
+
const identifier = String(value ?? "").trim().toUpperCase();
|
|
857
|
+
if (!identifier || identifier.length > 31 || !/^[A-Z_][A-Z0-9_$]*$/.test(identifier)) {
|
|
858
|
+
throw new Error(`${label} contains an invalid Firebird identifier.`);
|
|
859
|
+
}
|
|
860
|
+
return identifier;
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
const collectionUsesColumn = (item, columnName) =>
|
|
864
|
+
(item?.columns ?? []).some((column) => String(column ?? "").trim().toUpperCase() === columnName);
|
|
865
|
+
|
|
866
|
+
const checkUsesColumn = (check, columnName) => {
|
|
867
|
+
if (collectionUsesColumn(check, columnName)) return true;
|
|
868
|
+
return new RegExp(`(^|[^A-Z0-9_$])${columnName}([^A-Z0-9_$]|$)`).test(String(check?.expression ?? "").toUpperCase());
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
const validateProtectedMasterDataColumn = (actual, tableName, columnName) => {
|
|
872
|
+
if (["ID", "SUSPEND"].includes(columnName)) {
|
|
873
|
+
throw new Error(`Column "${columnName}" is protected and cannot be dropped from ${tableName}.`);
|
|
874
|
+
}
|
|
875
|
+
const primaryKey = new Set(
|
|
876
|
+
(actual?.primaryKey ?? []).map((column) => String(column ?? "").trim().toUpperCase()),
|
|
877
|
+
);
|
|
878
|
+
if (primaryKey.has(columnName)) {
|
|
879
|
+
throw new Error(`Column "${columnName}" is part of the primary key in ${tableName}.`);
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
const getIncomingMasterDataReferences = (actual, columnName) => (actual?.incomingForeignKeys ?? []).filter((foreignKey) =>
|
|
884
|
+
(foreignKey?.referenceColumns ?? []).some((column) => String(column ?? "").trim().toUpperCase() === columnName));
|
|
885
|
+
|
|
886
|
+
export const buildMasterDataColumnDropPlan = (actual, requestedColumnNames) => {
|
|
887
|
+
const tableName = normalizeScopedDropIdentifier(actual?.tableName, "Table name");
|
|
888
|
+
const requestedColumns = [...new Set((requestedColumnNames ?? []).map((name) => normalizeScopedDropIdentifier(name, "Column name")))];
|
|
889
|
+
const actualColumns = new Set(
|
|
890
|
+
(actual?.columns ?? []).map((column) => String(column?.name ?? "").trim().toUpperCase()),
|
|
891
|
+
);
|
|
892
|
+
const statements = [];
|
|
893
|
+
const droppedColumns = [];
|
|
894
|
+
const alreadyMissingColumns = [];
|
|
895
|
+
for (const columnName of requestedColumns) {
|
|
896
|
+
validateProtectedMasterDataColumn(actual, tableName, columnName);
|
|
897
|
+
if (!actualColumns.has(columnName)) {
|
|
898
|
+
alreadyMissingColumns.push(columnName);
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const incoming = getIncomingMasterDataReferences(actual, columnName);
|
|
903
|
+
if (incoming.length > 0) {
|
|
904
|
+
const names = incoming.map((item) => item.name).filter(Boolean).join(", ");
|
|
905
|
+
throw new Error(`Column "${columnName}" in ${tableName} is referenced by incoming foreign key ${names}.`);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const foreignKeys = (actual?.foreignKeys ?? []).filter((item) => collectionUsesColumn(item, columnName));
|
|
909
|
+
const checks = (actual?.checks ?? []).filter((item) => checkUsesColumn(item, columnName));
|
|
910
|
+
const uniqueKeys = (actual?.uniqueKeys ?? []).filter((item) => collectionUsesColumn(item, columnName));
|
|
911
|
+
const indexes = (actual?.indexes ?? []).filter((item) => collectionUsesColumn(item, columnName));
|
|
912
|
+
const removableNames = new Set(
|
|
913
|
+
[...foreignKeys, ...checks, ...uniqueKeys, ...indexes]
|
|
914
|
+
.flatMap((item) => [item?.name, item?.triggerName])
|
|
915
|
+
.map((name) => String(name ?? "").trim().toUpperCase())
|
|
916
|
+
.filter(Boolean),
|
|
917
|
+
);
|
|
918
|
+
const blockers = (actual?.columnDependencies ?? []).filter(
|
|
919
|
+
(dependency) =>
|
|
920
|
+
String(dependency?.columnName ?? "").trim().toUpperCase() === columnName &&
|
|
921
|
+
!removableNames.has(String(dependency?.dependentName ?? "").trim().toUpperCase()),
|
|
922
|
+
);
|
|
923
|
+
if (blockers.length > 0) {
|
|
924
|
+
const names = blockers.map((item) => item.dependentName).filter(Boolean).join(", ");
|
|
925
|
+
throw new Error(`Column "${columnName}" in ${tableName} has dependent database objects: ${names}.`);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
foreignKeys.forEach((item) => statements.push(`ALTER TABLE ${tableName} DROP CONSTRAINT ${item.name};`));
|
|
929
|
+
checks.forEach((item) => statements.push(`ALTER TABLE ${tableName} DROP CONSTRAINT ${item.name};`));
|
|
930
|
+
uniqueKeys.forEach((item) => statements.push(`ALTER TABLE ${tableName} DROP CONSTRAINT ${item.name};`));
|
|
931
|
+
indexes.forEach((item) => statements.push(`DROP INDEX ${item.name};`));
|
|
932
|
+
statements.push(`ALTER TABLE ${tableName} DROP ${columnName};`);
|
|
933
|
+
droppedColumns.push(columnName);
|
|
934
|
+
}
|
|
935
|
+
return {
|
|
936
|
+
statements: [...new Set(statements.map((statement) => toSingleLine(statement)))],
|
|
937
|
+
droppedColumns,
|
|
938
|
+
alreadyMissingColumns,
|
|
939
|
+
};
|
|
940
|
+
};
|
|
941
|
+
|
|
855
942
|
export const buildGeneratorStructureQueries = (table) => {
|
|
856
943
|
const generatorName = toDbName(table.generatorName ?? buildObjectName(table.name, "GEN"));
|
|
857
944
|
const triggerName = toDbName(table.triggerName ?? buildObjectName(table.name, "BI"));
|
|
@@ -979,6 +1066,51 @@ export const buildGeneratorSyncPlan = (table, generatorRows, triggerRows) => {
|
|
|
979
1066
|
};
|
|
980
1067
|
};
|
|
981
1068
|
|
|
1069
|
+
export const buildAppendOnlyStructureQueries = (table) => {
|
|
1070
|
+
const triggerName = toDbName(table.appendOnlyTriggerName ?? buildObjectName(table.name, "NOUPD"));
|
|
1071
|
+
return {
|
|
1072
|
+
exceptionSql: [
|
|
1073
|
+
"SELECT COUNT(*) AS EXCEPTION_EXISTS",
|
|
1074
|
+
"FROM RDB$EXCEPTIONS",
|
|
1075
|
+
`WHERE UPPER(TRIM(RDB$EXCEPTION_NAME)) = '${firebirdDdl.APPEND_ONLY_EXCEPTION_NAME}';`,
|
|
1076
|
+
].join(" "),
|
|
1077
|
+
triggerSql: [
|
|
1078
|
+
"SELECT COUNT(*) AS TRIGGER_EXISTS",
|
|
1079
|
+
"FROM RDB$TRIGGERS T",
|
|
1080
|
+
`WHERE UPPER(TRIM(T.RDB$TRIGGER_NAME)) = '${triggerName}'`,
|
|
1081
|
+
" AND COALESCE(T.RDB$SYSTEM_FLAG, 0) = 0;",
|
|
1082
|
+
].join(" "),
|
|
1083
|
+
};
|
|
1084
|
+
};
|
|
1085
|
+
|
|
1086
|
+
export const buildAppendOnlySyncPlan = (table, exceptionRows, triggerRows) => {
|
|
1087
|
+
if (!table?.appendOnly) {
|
|
1088
|
+
return { statements: [], differences: [] };
|
|
1089
|
+
}
|
|
1090
|
+
const triggerName = toDbName(table.appendOnlyTriggerName ?? buildObjectName(table.name, "NOUPD"));
|
|
1091
|
+
const statements = [];
|
|
1092
|
+
const differences = [];
|
|
1093
|
+
|
|
1094
|
+
const exceptionExists =
|
|
1095
|
+
Number(exceptionRows?.[0]?.["EXCEPTION_EXISTS"] ?? exceptionRows?.[0]?.["exception_exists"] ?? 0) === 1;
|
|
1096
|
+
const triggerExists =
|
|
1097
|
+
Number(triggerRows?.[0]?.["TRIGGER_EXISTS"] ?? triggerRows?.[0]?.["trigger_exists"] ?? 0) === 1;
|
|
1098
|
+
|
|
1099
|
+
if (!exceptionExists) {
|
|
1100
|
+
differences.push(`Exception "${firebirdDdl.APPEND_ONLY_EXCEPTION_NAME}" is missing.`);
|
|
1101
|
+
statements.push(firebirdDdl.buildCreateAppendOnlyExceptionStatement());
|
|
1102
|
+
}
|
|
1103
|
+
if (!triggerExists) {
|
|
1104
|
+
differences.push(`Append-only trigger "${triggerName}" is missing.`);
|
|
1105
|
+
statements.push(firebirdDdl.buildAppendOnlyTriggerStatement(table.name, triggerName));
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
return {
|
|
1109
|
+
statements: [...new Set(statements.map((statement) => toSingleLine(statement)))],
|
|
1110
|
+
differences,
|
|
1111
|
+
};
|
|
1112
|
+
};
|
|
1113
|
+
|
|
982
1114
|
export const buildDomainDdlPreview = (domainConfigContent, defaultDbIndex) => {
|
|
983
1115
|
const buildResult = buildDomainSchemaInternal(domainConfigContent, defaultDbIndex);
|
|
984
1116
|
const schema = buildResult.schema;
|
|
@@ -1286,6 +1418,11 @@ const buildEntityTable = (entityName, entity, defaultDbIndex) => {
|
|
|
1286
1418
|
table.triggerName = buildObjectName(tableName, "BI");
|
|
1287
1419
|
}
|
|
1288
1420
|
|
|
1421
|
+
if (entity?.appendOnly === true) {
|
|
1422
|
+
table.appendOnly = true;
|
|
1423
|
+
table.appendOnlyTriggerName = buildObjectName(tableName, "NOUPD");
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1289
1426
|
return {
|
|
1290
1427
|
entityName,
|
|
1291
1428
|
tableName,
|
|
@@ -1970,6 +2107,20 @@ const loadGeneratorSyncPlan = async (table, dbIndex, config) => {
|
|
|
1970
2107
|
return buildGeneratorSyncPlan(table, generatorRows, triggerRows);
|
|
1971
2108
|
};
|
|
1972
2109
|
|
|
2110
|
+
const loadAppendOnlySyncPlan = async (table, dbIndex, config) => {
|
|
2111
|
+
if (!table?.appendOnly) {
|
|
2112
|
+
return { statements: [], differences: [] };
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
const queries = buildAppendOnlyStructureQueries(table);
|
|
2116
|
+
const [exceptionRows, triggerRows] = await Promise.all([
|
|
2117
|
+
execRawSql(queries.exceptionSql, dbIndex, config),
|
|
2118
|
+
execRawSql(queries.triggerSql, dbIndex, config),
|
|
2119
|
+
]);
|
|
2120
|
+
|
|
2121
|
+
return buildAppendOnlySyncPlan(table, exceptionRows, triggerRows);
|
|
2122
|
+
};
|
|
2123
|
+
|
|
1973
2124
|
const toFirebird25CompatibleDdl = (statement) => {
|
|
1974
2125
|
const sanitizedStatement = sanitizeSql(statement);
|
|
1975
2126
|
const notNullMatch = sanitizedStatement.match(
|
|
@@ -2100,6 +2251,22 @@ const applyGeneratorSyncPlan = async (table, generatorSyncPlan, dbIndex, config,
|
|
|
2100
2251
|
return null;
|
|
2101
2252
|
};
|
|
2102
2253
|
|
|
2254
|
+
const applyAppendOnlySyncPlan = async (table, appendOnlySyncPlan, dbIndex, config, ctx) => {
|
|
2255
|
+
appendDryRunReport(ctx.report, ctx.migrationMode, table.name, "append-only-sync", {
|
|
2256
|
+
statements: appendOnlySyncPlan.statements,
|
|
2257
|
+
warnings: [],
|
|
2258
|
+
differences: appendOnlySyncPlan.differences,
|
|
2259
|
+
});
|
|
2260
|
+
if (appendOnlySyncPlan.statements.length === 0) {
|
|
2261
|
+
return null;
|
|
2262
|
+
}
|
|
2263
|
+
if (appendOnlySyncPlan.differences.length > 0) {
|
|
2264
|
+
console.log(`[AppConfig Publish] Table ${table.name} append-only:`, appendOnlySyncPlan.differences);
|
|
2265
|
+
}
|
|
2266
|
+
await executeSqlStatements(appendOnlySyncPlan.statements, dbIndex, config, ctx);
|
|
2267
|
+
return null;
|
|
2268
|
+
};
|
|
2269
|
+
|
|
2103
2270
|
const splitSqlStatements = (sqlBatch) => {
|
|
2104
2271
|
const source = sqlBatch ?? "";
|
|
2105
2272
|
const statements = [];
|
|
@@ -2166,6 +2333,9 @@ const syncTable = async (table, ctx) => {
|
|
|
2166
2333
|
const generatorSyncPlan = await loadGeneratorSyncPlan(table, tableDbIndex, ctx.config);
|
|
2167
2334
|
await applyGeneratorSyncPlan(table, generatorSyncPlan, tableDbIndex, ctx.config, ctx);
|
|
2168
2335
|
|
|
2336
|
+
const createAppendOnlyPlan = await loadAppendOnlySyncPlan(table, tableDbIndex, ctx.config);
|
|
2337
|
+
await applyAppendOnlySyncPlan(table, createAppendOnlyPlan, tableDbIndex, ctx.config, ctx);
|
|
2338
|
+
|
|
2169
2339
|
const latestSnapshot = await loadTableSnapshot(table.name, true, tableDbIndex, ctx.config);
|
|
2170
2340
|
const postCreateSyncPlan = buildTableSyncPlan(table, latestSnapshot, { migrationMode: ctx.migrationMode });
|
|
2171
2341
|
if (postCreateSyncPlan.warnings.length > 0) {
|
|
@@ -2186,6 +2356,9 @@ const syncTable = async (table, ctx) => {
|
|
|
2186
2356
|
const generatorSyncPlan = await loadGeneratorSyncPlan(table, tableDbIndex, ctx.config);
|
|
2187
2357
|
await applyGeneratorSyncPlan(table, generatorSyncPlan, tableDbIndex, ctx.config, ctx);
|
|
2188
2358
|
|
|
2359
|
+
const existingAppendOnlyPlan = await loadAppendOnlySyncPlan(table, tableDbIndex, ctx.config);
|
|
2360
|
+
await applyAppendOnlySyncPlan(table, existingAppendOnlyPlan, tableDbIndex, ctx.config, ctx);
|
|
2361
|
+
|
|
2189
2362
|
if (compareResult.isSame) {
|
|
2190
2363
|
return null;
|
|
2191
2364
|
}
|
|
@@ -2218,6 +2391,46 @@ export const resolveConfigFromPayload = (payload = {}, argv = {}) => {
|
|
|
2218
2391
|
};
|
|
2219
2392
|
};
|
|
2220
2393
|
|
|
2394
|
+
export const dropMasterDataColumns = async (payload = {}, argv = {}) => {
|
|
2395
|
+
try {
|
|
2396
|
+
const tableName = normalizeScopedDropIdentifier(payload?.tableName, "Table name");
|
|
2397
|
+
const columnNames = Array.isArray(payload?.columnNames) ? payload.columnNames : [];
|
|
2398
|
+
if (columnNames.length === 0) {
|
|
2399
|
+
return { success: true, tableName, droppedColumns: [], alreadyMissingColumns: [] };
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
const config = resolveConfigFromPayload(payload, argv);
|
|
2403
|
+
const dbIndex = resolveExecutionDbIndex(payload?.dbIndex, config);
|
|
2404
|
+
const normalizedColumns = [
|
|
2405
|
+
...new Set(columnNames.map((name) => normalizeScopedDropIdentifier(name, "Column name"))),
|
|
2406
|
+
];
|
|
2407
|
+
const exists = await loadTableExists(tableName, dbIndex, config);
|
|
2408
|
+
if (!exists) {
|
|
2409
|
+
return {
|
|
2410
|
+
success: true,
|
|
2411
|
+
tableName,
|
|
2412
|
+
droppedColumns: [],
|
|
2413
|
+
alreadyMissingColumns: normalizedColumns,
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
const snapshot = await loadTableSnapshot(tableName, true, dbIndex, config);
|
|
2418
|
+
const plan = buildMasterDataColumnDropPlan(snapshot, normalizedColumns);
|
|
2419
|
+
await executeSqlStatements(plan.statements, dbIndex, config, {
|
|
2420
|
+
dryRun: false,
|
|
2421
|
+
executedBlockSignatures: new Set(),
|
|
2422
|
+
});
|
|
2423
|
+
return {
|
|
2424
|
+
success: true,
|
|
2425
|
+
tableName,
|
|
2426
|
+
droppedColumns: plan.droppedColumns,
|
|
2427
|
+
alreadyMissingColumns: plan.alreadyMissingColumns,
|
|
2428
|
+
};
|
|
2429
|
+
} catch (error) {
|
|
2430
|
+
return { success: false, error: error?.message ?? String(error) };
|
|
2431
|
+
}
|
|
2432
|
+
};
|
|
2433
|
+
|
|
2221
2434
|
export const buildPublishStatusConfigKey = (domainName, domainVersion) => ({
|
|
2222
2435
|
name: `${domainName}__PUBLISH_STATUS`,
|
|
2223
2436
|
version: domainVersion,
|
|
@@ -77,6 +77,7 @@ export const buildTableStructureQueries = (tableName) => {
|
|
|
77
77
|
checkConstraintsSql: [
|
|
78
78
|
'SELECT',
|
|
79
79
|
' TRIM(rc.RDB$CONSTRAINT_NAME) AS CONSTRAINT_NAME,',
|
|
80
|
+
' TRIM(cc.RDB$TRIGGER_NAME) AS TRIGGER_NAME,',
|
|
80
81
|
' TRIM(CAST(t.RDB$TRIGGER_SOURCE AS VARCHAR(4096))) AS CHECK_SOURCE',
|
|
81
82
|
'FROM RDB$RELATION_CONSTRAINTS rc',
|
|
82
83
|
'JOIN RDB$CHECK_CONSTRAINTS cc ON cc.RDB$CONSTRAINT_NAME = rc.RDB$CONSTRAINT_NAME',
|
|
@@ -236,6 +237,7 @@ export const buildStructureSnapshotFromRows = (input) => {
|
|
|
236
237
|
|
|
237
238
|
export const generateExecuteBlockSql = (schema) => {
|
|
238
239
|
const statements = [];
|
|
240
|
+
let hasAppendOnly = false;
|
|
239
241
|
|
|
240
242
|
for (const table of schema.tables) {
|
|
241
243
|
const createTable = buildCreateTableStatement(table);
|
|
@@ -287,6 +289,18 @@ export const generateExecuteBlockSql = (schema) => {
|
|
|
287
289
|
),
|
|
288
290
|
);
|
|
289
291
|
}
|
|
292
|
+
|
|
293
|
+
if (table.appendOnly) {
|
|
294
|
+
hasAppendOnly = true;
|
|
295
|
+
statements.push(
|
|
296
|
+
buildAppendOnlyTriggerStatement(table.name, resolveAppendOnlyTriggerName(table)),
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (hasAppendOnly) {
|
|
302
|
+
// the shared exception must exist before any NOUPD trigger references it
|
|
303
|
+
statements.unshift(buildCreateAppendOnlyExceptionStatement());
|
|
290
304
|
}
|
|
291
305
|
|
|
292
306
|
return []
|
|
@@ -471,6 +485,30 @@ const resolveIdentityValueExpression = (table, identityColumn, generatorName) =>
|
|
|
471
485
|
return `GEN_ID(${normalizeIdentifier(generatorName)}, 1)`;
|
|
472
486
|
};
|
|
473
487
|
|
|
488
|
+
// Shared exception raised by every append-only table's NOUPD trigger.
|
|
489
|
+
export const APPEND_ONLY_EXCEPTION_NAME = 'E_APPEND_ONLY';
|
|
490
|
+
const APPEND_ONLY_EXCEPTION_MESSAGE = 'This table is append-only; UPDATE and DELETE are not allowed.';
|
|
491
|
+
|
|
492
|
+
export const buildCreateAppendOnlyExceptionStatement = () => {
|
|
493
|
+
return `CREATE EXCEPTION ${APPEND_ONLY_EXCEPTION_NAME} '${APPEND_ONLY_EXCEPTION_MESSAGE}'`;
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
// A BEFORE UPDATE OR DELETE trigger that makes the table append-only (raises E_APPEND_ONLY).
|
|
497
|
+
export const buildAppendOnlyTriggerStatement = (tableName, triggerName) => {
|
|
498
|
+
const normalizedTableName = normalizeIdentifier(tableName);
|
|
499
|
+
const normalizedTriggerName = normalizeIdentifier(triggerName);
|
|
500
|
+
return [
|
|
501
|
+
`CREATE TRIGGER ${normalizedTriggerName} FOR ${normalizedTableName} ACTIVE`,
|
|
502
|
+
'BEFORE UPDATE OR DELETE POSITION 0 AS BEGIN',
|
|
503
|
+
`EXCEPTION ${APPEND_ONLY_EXCEPTION_NAME};`,
|
|
504
|
+
'END',
|
|
505
|
+
].join(' ');
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
export const resolveAppendOnlyTriggerName = (table) => {
|
|
509
|
+
return normalizeIdentifier(table.appendOnlyTriggerName ?? `${table.name}_NOUPD`);
|
|
510
|
+
};
|
|
511
|
+
|
|
474
512
|
const wrapCreateIfMissing = (statement) => {
|
|
475
513
|
return [
|
|
476
514
|
'EXECUTE BLOCK AS',
|
|
@@ -832,6 +870,7 @@ const groupCheckConstraints = (rows) => {
|
|
|
832
870
|
return (rows ?? [])
|
|
833
871
|
.map((row) => ({
|
|
834
872
|
name: toUpperText(row['CONSTRAINT_NAME'] ?? row['constraint_name']),
|
|
873
|
+
triggerName: toUpperText(row['TRIGGER_NAME'] ?? row['trigger_name']),
|
|
835
874
|
expression: normalizeExpression(
|
|
836
875
|
row['CHECK_SOURCE'] ??
|
|
837
876
|
row['check_source'] ??
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { createLogger, transports, format } from "winston";
|
|
3
|
+
import { queryData } from "../db/db.js";
|
|
4
|
+
|
|
5
|
+
export const CLIENT_ROOM = "clientRoom";
|
|
6
|
+
const MAX_RETRIES = 3;
|
|
7
|
+
const RETRY_DELAY_MS = 10000; // 10 seconds
|
|
8
|
+
|
|
9
|
+
// 1. STRICT DLQ SCHEMA (Ready for DB imports later)
|
|
10
|
+
const dlqLogger = createLogger({
|
|
11
|
+
level: "error",
|
|
12
|
+
format: format.combine(format.timestamp(), format.json()),
|
|
13
|
+
transports: [new transports.File({ filename: "log/dlq.log" })],
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
class ClientEmitterService {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.ioServerInstance = null;
|
|
19
|
+
this.hubSocket = null;
|
|
20
|
+
this.subdomain = null;
|
|
21
|
+
|
|
22
|
+
// Map<clientInstanceId, Map<msgId, PendingData>>
|
|
23
|
+
this.clientQueues = new Map();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
init(ioServerInstance, hubSocket, subdomain) {
|
|
27
|
+
this.ioServerInstance = ioServerInstance;
|
|
28
|
+
this.hubSocket = hubSocket;
|
|
29
|
+
this.subdomain = subdomain;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Process the incoming ESB_ACK from the Angular Client
|
|
34
|
+
* PRUNE OLDER PENDING EVENTS
|
|
35
|
+
* Since we just received an ACK for this event, any older event in this queue is now officially a phantom/stale."
|
|
36
|
+
*/
|
|
37
|
+
acknowledgeMessage(msgId, clientInstanceId) {
|
|
38
|
+
const queue = this.clientQueues.get(clientInstanceId);
|
|
39
|
+
if (queue && queue.has(msgId)) {
|
|
40
|
+
const ackedEvent = queue.get(msgId);
|
|
41
|
+
|
|
42
|
+
for (const [id, pending] of queue.entries()) {
|
|
43
|
+
if (pending.timestamp <= ackedEvent.timestamp) {
|
|
44
|
+
clearTimeout(pending.timer);
|
|
45
|
+
queue.delete(id);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (queue.size === 0) this.clientQueues.delete(clientInstanceId); // Memory cleanup
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* TARGETED EMIT: Blasts to a specific client with Superseding Logic
|
|
55
|
+
*/
|
|
56
|
+
emitClient(clientInstanceId, eventName, payload, attempt = 1) {
|
|
57
|
+
const msgId = attempt === 1 ? crypto.randomUUID() : payload.msgId;
|
|
58
|
+
const creationTime = attempt === 1 ? Date.now() : payload.timestamp;
|
|
59
|
+
|
|
60
|
+
const wrappedPayload =
|
|
61
|
+
attempt === 1
|
|
62
|
+
? { msgId: msgId, timestamp: creationTime, data: payload }
|
|
63
|
+
: payload;
|
|
64
|
+
|
|
65
|
+
if (!this.clientQueues.has(clientInstanceId)) {
|
|
66
|
+
this.clientQueues.set(clientInstanceId, new Map());
|
|
67
|
+
}
|
|
68
|
+
const queue = this.clientQueues.get(clientInstanceId);
|
|
69
|
+
|
|
70
|
+
if (attempt === 1) {
|
|
71
|
+
// Supersede Logic: Kill older phantom retries of the exact same event type
|
|
72
|
+
for (const [existingMsgId, pending] of queue.entries()) {
|
|
73
|
+
if (
|
|
74
|
+
pending.eventName === eventName &&
|
|
75
|
+
pending.timestamp < creationTime
|
|
76
|
+
) {
|
|
77
|
+
console.log(
|
|
78
|
+
`[ESB] Superseding older '${eventName}' event for ${clientInstanceId}`,
|
|
79
|
+
);
|
|
80
|
+
clearTimeout(pending.timer);
|
|
81
|
+
queue.delete(existingMsgId);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// SETUP THE RETRY TIMER
|
|
87
|
+
const timer = setTimeout(() => {
|
|
88
|
+
if (queue.has(msgId)) {
|
|
89
|
+
if (attempt <= MAX_RETRIES) {
|
|
90
|
+
console.log(
|
|
91
|
+
`[ESB] Retry ${attempt}/${MAX_RETRIES} for event '${eventName}' to ${clientInstanceId}`,
|
|
92
|
+
);
|
|
93
|
+
this.emitClient(
|
|
94
|
+
clientInstanceId,
|
|
95
|
+
eventName,
|
|
96
|
+
wrappedPayload,
|
|
97
|
+
attempt + 1,
|
|
98
|
+
);
|
|
99
|
+
} else {
|
|
100
|
+
// DLQ RECORD FORMAT
|
|
101
|
+
const dlqRecord = {
|
|
102
|
+
msgId: msgId,
|
|
103
|
+
clientInstanceId: clientInstanceId,
|
|
104
|
+
eventName: eventName,
|
|
105
|
+
createdAt: creationTime,
|
|
106
|
+
failedAt: Date.now(),
|
|
107
|
+
retryCount: MAX_RETRIES,
|
|
108
|
+
payload: wrappedPayload.data,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
dlqLogger.error("ESB_DELIVERY_FAILURE", dlqRecord);
|
|
112
|
+
|
|
113
|
+
queue.delete(msgId);
|
|
114
|
+
if (queue.size === 0)
|
|
115
|
+
this.clientQueues.delete(clientInstanceId);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}, RETRY_DELAY_MS);
|
|
119
|
+
|
|
120
|
+
// Register the expectation and payload explicitly
|
|
121
|
+
queue.set(msgId, {
|
|
122
|
+
timer,
|
|
123
|
+
eventName,
|
|
124
|
+
timestamp: creationTime,
|
|
125
|
+
payload: wrappedPayload,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// FIRE TO THE DUAL CHANNELS
|
|
129
|
+
if (this.ioServerInstance) {
|
|
130
|
+
this.ioServerInstance
|
|
131
|
+
.to(clientInstanceId)
|
|
132
|
+
.emit(eventName, wrappedPayload);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (this.hubSocket && this.hubSocket.connected) {
|
|
136
|
+
this.hubSocket.emit("emitToClient", {
|
|
137
|
+
clientInstanceId,
|
|
138
|
+
eventName,
|
|
139
|
+
payload: wrappedPayload,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
broadcastClient(eventName, payload) {
|
|
145
|
+
const wrappedPayload = {
|
|
146
|
+
msgId: crypto.randomUUID(),
|
|
147
|
+
timestamp: Date.now(),
|
|
148
|
+
data: payload,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
if (this.ioServerInstance)
|
|
152
|
+
this.ioServerInstance
|
|
153
|
+
.to(CLIENT_ROOM)
|
|
154
|
+
.emit(eventName, wrappedPayload);
|
|
155
|
+
|
|
156
|
+
if (this.hubSocket && this.hubSocket.connected) {
|
|
157
|
+
this.hubSocket.emit("broadcastToClient", {
|
|
158
|
+
eventName,
|
|
159
|
+
payload: wrappedPayload,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Standardized Acknowledgment Emit
|
|
166
|
+
* Automatically checks if the frontend requested a targeted response.
|
|
167
|
+
*/
|
|
168
|
+
notifyRequester(payload, data) {
|
|
169
|
+
if (payload && payload.clientInstanceId && payload.eventName) {
|
|
170
|
+
this.emitClient(payload.clientInstanceId, payload.eventName, data);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async emitUsers(userIds, eventName, payload) {
|
|
175
|
+
if (!Array.isArray(userIds) || userIds.length === 0) return;
|
|
176
|
+
|
|
177
|
+
// Use Set to natively guarantee unique values
|
|
178
|
+
let targetInstances = new Set();
|
|
179
|
+
|
|
180
|
+
// 1. Fetch instances from directHub
|
|
181
|
+
if (this.ioServerInstance && this.subdomain) {
|
|
182
|
+
for (const uid of userIds) {
|
|
183
|
+
const roomName = `${this.subdomain}_USER_${uid}`;
|
|
184
|
+
const sockets = await this.ioServerInstance
|
|
185
|
+
.in(roomName)
|
|
186
|
+
.fetchSockets();
|
|
187
|
+
sockets.forEach((s) => {
|
|
188
|
+
const cid = s.handshake?.query?.clientInstanceId;
|
|
189
|
+
if (cid !== undefined && cid !== null) {
|
|
190
|
+
targetInstances.add(cid);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 2. Fetch instances from Hub Server
|
|
197
|
+
if (this.hubSocket && this.hubSocket.connected) {
|
|
198
|
+
try {
|
|
199
|
+
const cloudInstances = await new Promise((resolve, reject) => {
|
|
200
|
+
this.hubSocket
|
|
201
|
+
.timeout(5000)
|
|
202
|
+
.emit(
|
|
203
|
+
"clientInstancesByUserIds",
|
|
204
|
+
{ userIds },
|
|
205
|
+
(err, res) => {
|
|
206
|
+
if (err) reject(err);
|
|
207
|
+
else resolve(res);
|
|
208
|
+
},
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Explicitly deduplicate the dual-channel overlap
|
|
213
|
+
if (Array.isArray(cloudInstances)) {
|
|
214
|
+
cloudInstances.forEach((cid) => {
|
|
215
|
+
if (!targetInstances.has(cid)) {
|
|
216
|
+
targetInstances.add(cid);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
} catch (e) {
|
|
221
|
+
console.error(
|
|
222
|
+
"[ESB] Timeout/Error fetching instances from Hub Server",
|
|
223
|
+
e.message,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 3. Blast the Targeted Emits
|
|
229
|
+
if (targetInstances.size > 0) {
|
|
230
|
+
// console.log(
|
|
231
|
+
// `[ESB] Routing '${eventName}' to ${targetInstances.size} active instance(s) for users: ${userIds.join(", ")}`,
|
|
232
|
+
// );
|
|
233
|
+
for (const cid of targetInstances) {
|
|
234
|
+
this.emitClient(cid, eventName, payload);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* TARGETED EMIT BY GROUP: Queries the DB for User IDs, then hands off to emitUsers
|
|
241
|
+
*/
|
|
242
|
+
async emitUserGroups(groupIds, eventName, payload) {
|
|
243
|
+
if (!Array.isArray(groupIds) || groupIds.length === 0) return;
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
// 1. Construct the exact payload format for orm.pas
|
|
247
|
+
const dbPayload = {
|
|
248
|
+
distinct: "true", // Triggers the DISTINCT keyword in orm.pas
|
|
249
|
+
columns: [{ data: "SYS$UGMEMBER.USER_ID", key: "USER_ID" }],
|
|
250
|
+
filter: [
|
|
251
|
+
{
|
|
252
|
+
junction: "",
|
|
253
|
+
column: "SYS$UGMEMBER.SYS$USERGROUP_ID",
|
|
254
|
+
operator: "IN",
|
|
255
|
+
value1: `(${groupIds.join(",")})`, // Generates: IN (1,2,3)
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// 2. Execute the local LAN query via the DataSnap Adapter
|
|
261
|
+
const result = await queryData(dbPayload);
|
|
262
|
+
// 3. Extract the unique User IDs from the mapped result
|
|
263
|
+
if (Array.isArray(result) && result.length > 0) {
|
|
264
|
+
const extractedUserIds = result.map((row) => row.USER_ID);
|
|
265
|
+
|
|
266
|
+
// console.log(
|
|
267
|
+
// `[ESB] emitUserGroups mapped ${groupIds.length} group(s) to ${extractedUserIds.length} unique user(s).`,
|
|
268
|
+
// );
|
|
269
|
+
|
|
270
|
+
// 4. Hand off to the proven emitUsers Strict DLQ engine
|
|
271
|
+
if (extractedUserIds.length > 0) {
|
|
272
|
+
await this.emitUsers(extractedUserIds, eventName, payload);
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
// console.log(
|
|
276
|
+
// `[ESB] emitUserGroups found no active users for groups: ${groupIds.join(", ")}`,
|
|
277
|
+
// );
|
|
278
|
+
}
|
|
279
|
+
} catch (err) {
|
|
280
|
+
console.error(
|
|
281
|
+
"[ESB] emitUserGroups Query Error:",
|
|
282
|
+
err.message || err,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export const ClientEmitter = new ClientEmitterService();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
2
|
import { workerEvents } from "../worker/cliWorkerPool.js";
|
|
3
|
+
import { ClientEmitter } from "./clientEmitter.js";
|
|
3
4
|
|
|
4
5
|
export class CLI_Dispatcher {
|
|
5
6
|
constructor(mode, ioServerInstance = null) {
|
|
@@ -52,31 +53,39 @@ export class CLI_Dispatcher {
|
|
|
52
53
|
});
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
workerEvents.on("IPC_RELAY", (topic,
|
|
56
|
-
this.balanceAndDispatchEvent(topic,
|
|
56
|
+
workerEvents.on("IPC_RELAY", (topic, data) => {
|
|
57
|
+
this.balanceAndDispatchEvent(topic, data);
|
|
57
58
|
});
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
balanceAndDispatchEvent(topic, payload) {
|
|
61
62
|
// --- EMIT TO SPECIFIC BIZ-A CLIENT ---
|
|
62
|
-
// ex: this.lib.emitClient(scriptData.
|
|
63
|
+
// ex: this.lib.emitClient(scriptData.clientInstanceId, scriptData.eventName, { status: "Done!" });
|
|
63
64
|
if (topic === "emitClient") {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
);
|
|
67
|
-
console.log(`[Future Dev] Event: ${payload?.eventUUIDName}`);
|
|
68
|
-
console.log(`[Future Dev] Payload:`, payload?.payload, `\n`);
|
|
69
|
-
// TODO: this.agentNamespace.to(payload.clientSockId).emit(payload.eventUUIDName, payload.payload);
|
|
65
|
+
const { clientInstanceId, eventName, data } = payload;
|
|
66
|
+
ClientEmitter.emitClient(clientInstanceId, eventName, data);
|
|
70
67
|
return;
|
|
71
68
|
}
|
|
72
69
|
|
|
73
70
|
// --- EMIT TO ALL BIZ-A CLIENT ---
|
|
74
71
|
// ex: this.lib.broadcastClient("ui:alert", { message: "System maintenance in 5 mins" });
|
|
75
72
|
if (topic === "broadcastClient") {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
const { eventName, data } = payload;
|
|
74
|
+
ClientEmitter.broadcastClient(eventName, data);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// --- EMIT BY USERS ---
|
|
79
|
+
if (topic === "emitUsers") {
|
|
80
|
+
const { userIds, eventName, data } = payload;
|
|
81
|
+
ClientEmitter.emitUsers(userIds, eventName, data);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// --- EMIT BY USER GROUPS ---
|
|
86
|
+
if (topic === "emitUserGroups") {
|
|
87
|
+
const { groupIds, eventName, data } = payload;
|
|
88
|
+
ClientEmitter.emitUserGroups(groupIds, eventName, data);
|
|
80
89
|
return;
|
|
81
90
|
}
|
|
82
91
|
|
package/package.json
CHANGED
package/scheduler/datalib.js
CHANGED
|
@@ -5,6 +5,7 @@ import * as dayjs from "dayjs";
|
|
|
5
5
|
import timezone from "dayjs/plugin/timezone.js";
|
|
6
6
|
import utc from "dayjs/plugin/utc.js";
|
|
7
7
|
import crypto from "crypto";
|
|
8
|
+
import { secretProvider } from "../security/secretProvider.js";
|
|
8
9
|
import { createLogger, transports, format } from "winston";
|
|
9
10
|
import { execCLIScriptWorker } from "../worker/cliWorkerPool.js";
|
|
10
11
|
|
|
@@ -199,6 +200,23 @@ export async function extractFunctionScript(
|
|
|
199
200
|
axios: axiosInstance, //SCY BZ 4316
|
|
200
201
|
dayjs: dayjs.default,
|
|
201
202
|
crypto: crypto,
|
|
203
|
+
// Keyed HMAC by NAME. The key never enters the CLI script — it stays in the
|
|
204
|
+
// platform secret provider (file -> DPAPI -> Vault, swappable). Fails closed.
|
|
205
|
+
hmac: (name, data) => {
|
|
206
|
+
const { key } = secretProvider.resolveForSign(name);
|
|
207
|
+
return crypto.createHmac("sha256", key).update(String(data)).digest("hex");
|
|
208
|
+
},
|
|
209
|
+
// Rotation-aware verify: resolves the key by the row's stored keyId.
|
|
210
|
+
hmacVerify: (name, data, digest, keyId) => {
|
|
211
|
+
const { key } = secretProvider.resolveForVerify(name, keyId);
|
|
212
|
+
const expected = crypto
|
|
213
|
+
.createHmac("sha256", key)
|
|
214
|
+
.update(String(data))
|
|
215
|
+
.digest("hex");
|
|
216
|
+
const a = Buffer.from(expected, "hex");
|
|
217
|
+
const b = Buffer.from(String(digest || ""), "hex");
|
|
218
|
+
return a.length === b.length && crypto.timingSafeEqual(a, b);
|
|
219
|
+
},
|
|
202
220
|
log: logger,
|
|
203
221
|
data: { sendModel, queryData, crudData, genId, getUrlAndParam },
|
|
204
222
|
...extraLib,
|
|
@@ -71,20 +71,38 @@ export const run = async (apiConfig, scriptIdentifier, scriptData) => {
|
|
|
71
71
|
};
|
|
72
72
|
|
|
73
73
|
// 2. Targeted Client UI Update
|
|
74
|
-
lib.emitClient = (
|
|
74
|
+
lib.emitClient = (clientInstanceId, eventName, data) => {
|
|
75
75
|
workerpool.workerEmit({
|
|
76
76
|
type: "IPC_RELAY",
|
|
77
77
|
topic: "emitClient",
|
|
78
|
-
payload: {
|
|
78
|
+
payload: { clientInstanceId, eventName, data },
|
|
79
79
|
});
|
|
80
80
|
};
|
|
81
81
|
|
|
82
82
|
// 3. Broadcast Client UI Update
|
|
83
|
-
lib.broadcastClient = (
|
|
83
|
+
lib.broadcastClient = (eventName, data) => {
|
|
84
84
|
workerpool.workerEmit({
|
|
85
85
|
type: "IPC_RELAY",
|
|
86
86
|
topic: "broadcastClient",
|
|
87
|
-
payload: {
|
|
87
|
+
payload: { eventName, data },
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// 4. Targeted by Users
|
|
92
|
+
lib.emitUsers = (userIds, eventName, data) => {
|
|
93
|
+
workerpool.workerEmit({
|
|
94
|
+
type: "IPC_RELAY",
|
|
95
|
+
topic: "emitUsers",
|
|
96
|
+
payload: { userIds, eventName, data },
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// 5. Targeted by User Groups
|
|
101
|
+
lib.emitUserGroups = (groupIds, eventName, data) => {
|
|
102
|
+
workerpool.workerEmit({
|
|
103
|
+
type: "IPC_RELAY",
|
|
104
|
+
topic: "emitUserGroups",
|
|
105
|
+
payload: { groupIds, eventName, data },
|
|
88
106
|
});
|
|
89
107
|
};
|
|
90
108
|
};
|
|
@@ -100,12 +118,15 @@ export const run = async (apiConfig, scriptIdentifier, scriptData) => {
|
|
|
100
118
|
db.save(payload, mergeConfig(customCfg)),
|
|
101
119
|
remove: (payload, customCfg) =>
|
|
102
120
|
db.remove(payload, mergeConfig(customCfg)),
|
|
103
|
-
|
|
104
|
-
db.
|
|
121
|
+
removeMD: (id, master, details, customCfg) =>
|
|
122
|
+
db.removeMD(id, master, details, mergeConfig(customCfg)),
|
|
105
123
|
list: (payload, customCfg) =>
|
|
106
124
|
db.list(payload, mergeConfig(customCfg)),
|
|
107
125
|
executeBlock: (sql, customCfg) =>
|
|
108
126
|
db.executeBlock(sql, mergeConfig(customCfg)),
|
|
127
|
+
ormGet: (payload, customCfg) =>
|
|
128
|
+
db.ormGet(payload, mergeConfig(customCfg)),
|
|
129
|
+
getGlobalConfig: () => db.getGlobalConfig(),
|
|
109
130
|
};
|
|
110
131
|
};
|
|
111
132
|
|
package/worker/cliWorkerPool.js
CHANGED
|
@@ -15,7 +15,10 @@ export const cliScriptWorkerPool = workerpool.pool(
|
|
|
15
15
|
workerType: "thread",
|
|
16
16
|
emitStdStreams: true, // the worker will emit stdout and stderr events instead of passing it through to the parent streams
|
|
17
17
|
workerThreadOpts: {
|
|
18
|
-
env
|
|
18
|
+
// worker env is a whitelist (not the full process.env). No application secrets here:
|
|
19
|
+
// CLI scripts get keyed HMAC via lib.hmac(name,...), resolved by the platform secret
|
|
20
|
+
// provider from the secrets store. BIZA_SECRETS_FILE (optional) points at that store.
|
|
21
|
+
env: { NODE_ENV: process.env.NODE_ENV || "production", BIZA_SECRETS_FILE: process.env.BIZA_SECRETS_FILE },
|
|
19
22
|
},
|
|
20
23
|
},
|
|
21
24
|
);
|