biz-a-cli 2.3.80-15230 → 2.3.80-15238
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 +13 -3
- package/bin/hub.js +3 -0
- package/bin/hubEvent.js +7 -0
- 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 +127 -0
- package/engine/domain/firebird-ddl.js +2 -0
- package/message-broker/clientEmitter.js +171 -0
- package/message-broker/dispatcher.js +8 -13
- package/package.json +1 -1
- package/worker/cliScriptWorker.js +9 -6
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,16 @@ export function directHubEvent(serverSocket, argv) {
|
|
|
69
69
|
setConnectListeners(clientSocket);
|
|
70
70
|
clientListener(clientSocket, argv);
|
|
71
71
|
clientSocket.join(CLIENT_ROOM);
|
|
72
|
+
|
|
73
|
+
// for ESB clientEmitter
|
|
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
|
+
}
|
|
72
82
|
} else if (clientSocket.handshake.query.isDeploy) {
|
|
73
83
|
setConnectListeners(clientSocket);
|
|
74
84
|
deploymentListenerForVSCode(clientSocket, argv);
|
|
@@ -89,7 +99,7 @@ export function createSocketServer(httpServer, cliIpAddress = "127.0.0.1") {
|
|
|
89
99
|
/\.vscode-cdn\.net$/,
|
|
90
100
|
].concat(
|
|
91
101
|
process.env.NODE_ENV === "production"
|
|
92
|
-
? []
|
|
102
|
+
? [`http://${cliIpAddress}:4200`, "http://localhost:4200", "http://localhost"]
|
|
93
103
|
: [`http://${cliIpAddress}:4200`, "http://localhost:4200"],
|
|
94
104
|
),
|
|
95
105
|
},
|
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);
|
|
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
|
|
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"));
|
|
@@ -2218,6 +2305,46 @@ export const resolveConfigFromPayload = (payload = {}, argv = {}) => {
|
|
|
2218
2305
|
};
|
|
2219
2306
|
};
|
|
2220
2307
|
|
|
2308
|
+
export const dropMasterDataColumns = async (payload = {}, argv = {}) => {
|
|
2309
|
+
try {
|
|
2310
|
+
const tableName = normalizeScopedDropIdentifier(payload?.tableName, "Table name");
|
|
2311
|
+
const columnNames = Array.isArray(payload?.columnNames) ? payload.columnNames : [];
|
|
2312
|
+
if (columnNames.length === 0) {
|
|
2313
|
+
return { success: true, tableName, droppedColumns: [], alreadyMissingColumns: [] };
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
const config = resolveConfigFromPayload(payload, argv);
|
|
2317
|
+
const dbIndex = resolveExecutionDbIndex(payload?.dbIndex, config);
|
|
2318
|
+
const normalizedColumns = [
|
|
2319
|
+
...new Set(columnNames.map((name) => normalizeScopedDropIdentifier(name, "Column name"))),
|
|
2320
|
+
];
|
|
2321
|
+
const exists = await loadTableExists(tableName, dbIndex, config);
|
|
2322
|
+
if (!exists) {
|
|
2323
|
+
return {
|
|
2324
|
+
success: true,
|
|
2325
|
+
tableName,
|
|
2326
|
+
droppedColumns: [],
|
|
2327
|
+
alreadyMissingColumns: normalizedColumns,
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
const snapshot = await loadTableSnapshot(tableName, true, dbIndex, config);
|
|
2332
|
+
const plan = buildMasterDataColumnDropPlan(snapshot, normalizedColumns);
|
|
2333
|
+
await executeSqlStatements(plan.statements, dbIndex, config, {
|
|
2334
|
+
dryRun: false,
|
|
2335
|
+
executedBlockSignatures: new Set(),
|
|
2336
|
+
});
|
|
2337
|
+
return {
|
|
2338
|
+
success: true,
|
|
2339
|
+
tableName,
|
|
2340
|
+
droppedColumns: plan.droppedColumns,
|
|
2341
|
+
alreadyMissingColumns: plan.alreadyMissingColumns,
|
|
2342
|
+
};
|
|
2343
|
+
} catch (error) {
|
|
2344
|
+
return { success: false, error: error?.message ?? String(error) };
|
|
2345
|
+
}
|
|
2346
|
+
};
|
|
2347
|
+
|
|
2221
2348
|
export const buildPublishStatusConfigKey = (domainName, domainVersion) => ({
|
|
2222
2349
|
name: `${domainName}__PUBLISH_STATUS`,
|
|
2223
2350
|
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',
|
|
@@ -832,6 +833,7 @@ const groupCheckConstraints = (rows) => {
|
|
|
832
833
|
return (rows ?? [])
|
|
833
834
|
.map((row) => ({
|
|
834
835
|
name: toUpperText(row['CONSTRAINT_NAME'] ?? row['constraint_name']),
|
|
836
|
+
triggerName: toUpperText(row['TRIGGER_NAME'] ?? row['trigger_name']),
|
|
835
837
|
expression: normalizeExpression(
|
|
836
838
|
row['CHECK_SOURCE'] ??
|
|
837
839
|
row['check_source'] ??
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { createLogger, transports, format } from "winston";
|
|
3
|
+
|
|
4
|
+
export const CLIENT_ROOM = "clientRoom";
|
|
5
|
+
const MAX_RETRIES = 3;
|
|
6
|
+
const RETRY_DELAY_MS = 10000; // 10 seconds
|
|
7
|
+
|
|
8
|
+
// 1. STRICT DLQ SCHEMA (Ready for DB imports later)
|
|
9
|
+
const dlqLogger = createLogger({
|
|
10
|
+
level: "error",
|
|
11
|
+
format: format.combine(format.timestamp(), format.json()),
|
|
12
|
+
transports: [new transports.File({ filename: "log/dlq.log" })],
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
class ClientEmitterService {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.ioServerInstance = null;
|
|
18
|
+
this.hubSocket = null;
|
|
19
|
+
|
|
20
|
+
// Map<clientInstanceId, Map<msgId, PendingData>>
|
|
21
|
+
this.clientQueues = new Map();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
init(ioServerInstance, hubSocket) {
|
|
25
|
+
this.ioServerInstance = ioServerInstance;
|
|
26
|
+
this.hubSocket = hubSocket;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Process the incoming ESB_ACK from the Angular Client
|
|
31
|
+
* PRUNE OLDER PENDING EVENTS
|
|
32
|
+
* Since we just received an ACK for this event, any older event in this queue is now officially a phantom/stale."
|
|
33
|
+
*/
|
|
34
|
+
acknowledgeMessage(msgId, clientInstanceId) {
|
|
35
|
+
const queue = this.clientQueues.get(clientInstanceId);
|
|
36
|
+
if (queue && queue.has(msgId)) {
|
|
37
|
+
const ackedEvent = queue.get(msgId);
|
|
38
|
+
|
|
39
|
+
for (const [id, pending] of queue.entries()) {
|
|
40
|
+
if (pending.timestamp <= ackedEvent.timestamp) {
|
|
41
|
+
clearTimeout(pending.timer);
|
|
42
|
+
queue.delete(id);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (queue.size === 0) this.clientQueues.delete(clientInstanceId); // Memory cleanup
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* TARGETED EMIT: Blasts to a specific client with Superseding Logic
|
|
52
|
+
*/
|
|
53
|
+
emitClient(clientInstanceId, eventName, payload, attempt = 1) {
|
|
54
|
+
const msgId = attempt === 1 ? crypto.randomUUID() : payload.msgId;
|
|
55
|
+
const creationTime = attempt === 1 ? Date.now() : payload.timestamp;
|
|
56
|
+
|
|
57
|
+
const wrappedPayload =
|
|
58
|
+
attempt === 1
|
|
59
|
+
? { msgId: msgId, timestamp: creationTime, data: payload }
|
|
60
|
+
: payload;
|
|
61
|
+
|
|
62
|
+
if (!this.clientQueues.has(clientInstanceId)) {
|
|
63
|
+
this.clientQueues.set(clientInstanceId, new Map());
|
|
64
|
+
}
|
|
65
|
+
const queue = this.clientQueues.get(clientInstanceId);
|
|
66
|
+
|
|
67
|
+
if (attempt === 1) {
|
|
68
|
+
// Supersede Logic: Kill older phantom retries of the exact same event type
|
|
69
|
+
for (const [existingMsgId, pending] of queue.entries()) {
|
|
70
|
+
if (
|
|
71
|
+
pending.eventName === eventName &&
|
|
72
|
+
pending.timestamp < creationTime
|
|
73
|
+
) {
|
|
74
|
+
console.log(
|
|
75
|
+
`[ESB] Superseding older '${eventName}' event for ${clientInstanceId}`,
|
|
76
|
+
);
|
|
77
|
+
clearTimeout(pending.timer);
|
|
78
|
+
queue.delete(existingMsgId);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// SETUP THE RETRY TIMER
|
|
84
|
+
const timer = setTimeout(() => {
|
|
85
|
+
if (queue.has(msgId)) {
|
|
86
|
+
if (attempt <= MAX_RETRIES) {
|
|
87
|
+
console.log(
|
|
88
|
+
`[ESB] Retry ${attempt}/${MAX_RETRIES} for event '${eventName}' to ${clientInstanceId}`,
|
|
89
|
+
);
|
|
90
|
+
this.emitClient(
|
|
91
|
+
clientInstanceId,
|
|
92
|
+
eventName,
|
|
93
|
+
wrappedPayload,
|
|
94
|
+
attempt + 1,
|
|
95
|
+
);
|
|
96
|
+
} else {
|
|
97
|
+
// DLQ RECORD FORMAT
|
|
98
|
+
const dlqRecord = {
|
|
99
|
+
msgId: msgId,
|
|
100
|
+
clientInstanceId: clientInstanceId,
|
|
101
|
+
eventName: eventName,
|
|
102
|
+
createdAt: creationTime,
|
|
103
|
+
failedAt: Date.now(),
|
|
104
|
+
retryCount: MAX_RETRIES,
|
|
105
|
+
payload: wrappedPayload.data,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
dlqLogger.error("ESB_DELIVERY_FAILURE", dlqRecord);
|
|
109
|
+
|
|
110
|
+
queue.delete(msgId);
|
|
111
|
+
if (queue.size === 0)
|
|
112
|
+
this.clientQueues.delete(clientInstanceId);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}, RETRY_DELAY_MS);
|
|
116
|
+
|
|
117
|
+
// Register the expectation and payload explicitly
|
|
118
|
+
queue.set(msgId, {
|
|
119
|
+
timer,
|
|
120
|
+
eventName,
|
|
121
|
+
timestamp: creationTime,
|
|
122
|
+
payload: wrappedPayload,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// FIRE TO THE DUAL CHANNELS
|
|
126
|
+
if (this.ioServerInstance) {
|
|
127
|
+
this.ioServerInstance
|
|
128
|
+
.to(clientInstanceId)
|
|
129
|
+
.emit(eventName, wrappedPayload);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (this.hubSocket && this.hubSocket.connected) {
|
|
133
|
+
this.hubSocket.emit("emitToClient", {
|
|
134
|
+
clientInstanceId,
|
|
135
|
+
eventName,
|
|
136
|
+
payload: wrappedPayload,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
broadcastClient(eventName, payload) {
|
|
142
|
+
const wrappedPayload = {
|
|
143
|
+
msgId: crypto.randomUUID(),
|
|
144
|
+
timestamp: Date.now(),
|
|
145
|
+
data: payload,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
if (this.ioServerInstance)
|
|
149
|
+
this.ioServerInstance
|
|
150
|
+
.to(CLIENT_ROOM)
|
|
151
|
+
.emit(eventName, wrappedPayload);
|
|
152
|
+
if (this.hubSocket && this.hubSocket.connected) {
|
|
153
|
+
this.hubSocket.emit("broadcastToClient", {
|
|
154
|
+
eventName,
|
|
155
|
+
payload: wrappedPayload,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Standardized Acknowledgment Emit
|
|
162
|
+
* Automatically checks if the frontend requested a targeted response.
|
|
163
|
+
*/
|
|
164
|
+
notifyRequester(payload, data) {
|
|
165
|
+
if (payload && payload.clientInstanceId && payload.eventName) {
|
|
166
|
+
this.emitClient(payload.clientInstanceId, payload.eventName, data);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
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,25 @@ 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
|
-
console.log(`[Future Dev] Payload:`, payload?.payload, `\n`);
|
|
79
|
-
// TODO: this.agentNamespace.to("clientRoom").emit(payload.topic, payload.payload);
|
|
73
|
+
const { eventName, data } = payload;
|
|
74
|
+
ClientEmitter.broadcastClient(eventName, data);
|
|
80
75
|
return;
|
|
81
76
|
}
|
|
82
77
|
|
package/package.json
CHANGED
|
@@ -71,20 +71,20 @@ 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
88
|
});
|
|
89
89
|
};
|
|
90
90
|
};
|
|
@@ -100,12 +100,15 @@ export const run = async (apiConfig, scriptIdentifier, scriptData) => {
|
|
|
100
100
|
db.save(payload, mergeConfig(customCfg)),
|
|
101
101
|
remove: (payload, customCfg) =>
|
|
102
102
|
db.remove(payload, mergeConfig(customCfg)),
|
|
103
|
-
|
|
104
|
-
db.
|
|
103
|
+
removeMD: (id, master, details, customCfg) =>
|
|
104
|
+
db.removeMD(id, master, details, mergeConfig(customCfg)),
|
|
105
105
|
list: (payload, customCfg) =>
|
|
106
106
|
db.list(payload, mergeConfig(customCfg)),
|
|
107
107
|
executeBlock: (sql, customCfg) =>
|
|
108
108
|
db.executeBlock(sql, mergeConfig(customCfg)),
|
|
109
|
+
ormGet: (payload, customCfg) =>
|
|
110
|
+
db.ormGet(payload, mergeConfig(customCfg)),
|
|
111
|
+
getGlobalConfig: () => db.getGlobalConfig(),
|
|
109
112
|
};
|
|
110
113
|
};
|
|
111
114
|
|