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.
@@ -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 './directHubEvent.js';
4
+ import { CLIENT_ROOM } from '../message-broker/clientEmitter.js';
5
5
 
6
6
  export async function getAppData(apiOpts){
7
7
  return await axios.request({
@@ -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
- export const CLIENT_ROOM = "clientRoom";
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 deleteMD = async (id, masterTable, detailTables, config = {}) => {
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
- deleteMD,
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
- id: null,
619
- sys$bpm_wf_instance_id: Number(instanceId),
620
- sys$bpm_trans_def_id: transition.id,
621
- effect_type: eff.effect_type,
622
- event_name: eff.event_name,
623
- payload_evaluated: eff.payload_template || null,
624
- status: "PENDING",
625
- retry_count: 0,
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 deleteMD(
818
+ const result = await removeMD(
739
819
  Number(instanceId),
740
820
  "SYS$BPM_WF_INSTANCE",
741
821
  detailTables,
742
822
  apiConfig,
743
823
  );
744
- return result.data;
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 && typeof parsedPayload === "object" && !Array.isArray(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 ---
@@ -1,4 +1,36 @@
1
- import { save as dbSave, queryData, deleteMD } from "../../db/db.js";
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 deleteMD(
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
- return result.data;
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, payload) => {
56
- this.balanceAndDispatchEvent(topic, payload);
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.clientSockId, scriptData.eventUUIDName, { status: "Done!" });
63
+ // ex: this.lib.emitClient(scriptData.clientInstanceId, scriptData.eventName, { status: "Done!" });
63
64
  if (topic === "emitClient") {
64
- console.log(
65
- `\n[Future Dev] Target: Biz-A Client Socket ID [${payload?.clientSockId}]`,
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
- console.log(`\n[Future Dev] Broadcast: ALL Clients in CLIENT_ROOM`);
77
- console.log(`[Future Dev] Event: ${payload?.topic}`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "biz-a-cli",
3
- "version": "2.3.80-15230",
3
+ "version": "2.3.80-15238",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "type": "module",
@@ -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 = (clientSockId, eventUUIDName, payload) => {
74
+ lib.emitClient = (clientInstanceId, eventName, data) => {
75
75
  workerpool.workerEmit({
76
76
  type: "IPC_RELAY",
77
77
  topic: "emitClient",
78
- payload: { clientSockId, eventUUIDName, payload },
78
+ payload: { clientInstanceId, eventName, data },
79
79
  });
80
80
  };
81
81
 
82
82
  // 3. Broadcast Client UI Update
83
- lib.broadcastClient = (topic, payload) => {
83
+ lib.broadcastClient = (eventName, data) => {
84
84
  workerpool.workerEmit({
85
85
  type: "IPC_RELAY",
86
86
  topic: "broadcastClient",
87
- payload: { topic, 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
- deleteMD: (id, master, details, customCfg) =>
104
- db.deleteMD(id, master, details, mergeConfig(customCfg)),
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