biz-a-cli 2.3.80-15238 → 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/directHubEvent.js +16 -2
- package/bin/hub.js +1 -1
- package/bin/hubEvent.js +32 -1
- package/engine/domain/appConfig.js +86 -0
- package/engine/domain/firebird-ddl.js +37 -0
- package/message-broker/clientEmitter.js +118 -1
- package/message-broker/dispatcher.js +14 -0
- package/package.json +1 -1
- package/scheduler/datalib.js +18 -0
- package/worker/cliScriptWorker.js +18 -0
- package/worker/cliWorkerPool.js +4 -1
package/bin/directHubEvent.js
CHANGED
|
@@ -70,7 +70,7 @@ export function directHubEvent(serverSocket, argv) {
|
|
|
70
70
|
clientListener(clientSocket, argv);
|
|
71
71
|
clientSocket.join(CLIENT_ROOM);
|
|
72
72
|
|
|
73
|
-
//
|
|
73
|
+
// isolated Client in a room using clientInstanceId
|
|
74
74
|
const clientInstanceId =
|
|
75
75
|
clientSocket.handshake.query.clientInstanceId;
|
|
76
76
|
if (clientInstanceId) {
|
|
@@ -79,6 +79,16 @@ export function directHubEvent(serverSocket, argv) {
|
|
|
79
79
|
ClientEmitter.acknowledgeMessage(msgId, clientInstanceId);
|
|
80
80
|
});
|
|
81
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
|
+
}
|
|
82
92
|
} else if (clientSocket.handshake.query.isDeploy) {
|
|
83
93
|
setConnectListeners(clientSocket);
|
|
84
94
|
deploymentListenerForVSCode(clientSocket, argv);
|
|
@@ -99,7 +109,11 @@ export function createSocketServer(httpServer, cliIpAddress = "127.0.0.1") {
|
|
|
99
109
|
/\.vscode-cdn\.net$/,
|
|
100
110
|
].concat(
|
|
101
111
|
process.env.NODE_ENV === "production"
|
|
102
|
-
? [
|
|
112
|
+
? [
|
|
113
|
+
`http://${cliIpAddress}:4200`,
|
|
114
|
+
"http://localhost:4200",
|
|
115
|
+
"http://localhost",
|
|
116
|
+
]
|
|
103
117
|
: [`http://${cliIpAddress}:4200`, "http://localhost:4200"],
|
|
104
118
|
),
|
|
105
119
|
},
|
package/bin/hub.js
CHANGED
|
@@ -295,7 +295,7 @@ if (argv.mode.trim().toLowerCase() === "agent") {
|
|
|
295
295
|
// directHubEvent(createSocketServer(server, argv.cliAddress().ip), argv);
|
|
296
296
|
directHubEvent(ioServerInstance, argv);
|
|
297
297
|
|
|
298
|
-
ClientEmitter.init(ioServerInstance, socket);
|
|
298
|
+
ClientEmitter.init(ioServerInstance, socket, argv.subdomain);
|
|
299
299
|
|
|
300
300
|
bootMicroservices(argv, ioServerInstance);
|
|
301
301
|
|
package/bin/hubEvent.js
CHANGED
|
@@ -305,7 +305,8 @@ const EngineRegistry = {
|
|
|
305
305
|
"bpm/workflowRT": () => import("../engine/bpm/workflow-runtime.js"),
|
|
306
306
|
"domain/system": () => import("../engine/domain/system.js"),
|
|
307
307
|
"domain/appConfig": () => import("../engine/domain/appConfig.js"),
|
|
308
|
-
"domain/applicationConfig": () =>
|
|
308
|
+
"domain/applicationConfig": () =>
|
|
309
|
+
import("../engine/domain/applicationConfig.js"),
|
|
309
310
|
};
|
|
310
311
|
|
|
311
312
|
const handleCliCommand = async (data, cb, argv) => {
|
|
@@ -364,12 +365,42 @@ const handleCliCommand = async (data, cb, argv) => {
|
|
|
364
365
|
cb(null, { success: true, data: result });
|
|
365
366
|
break;
|
|
366
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
|
+
}
|
|
367
391
|
|
|
392
|
+
cb(null, {
|
|
393
|
+
success: true,
|
|
394
|
+
message: `Handed over ${action} to ESB ClientEmitter`,
|
|
395
|
+
});
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
368
398
|
default:
|
|
369
399
|
cb(`Unknown CLI command '${command}'`, null);
|
|
370
400
|
}
|
|
371
401
|
} catch (err) {
|
|
372
402
|
cb(err.message || err, null);
|
|
403
|
+
// console.error("[CLICommand] Error:", err);
|
|
373
404
|
}
|
|
374
405
|
};
|
|
375
406
|
|
|
@@ -1066,6 +1066,51 @@ export const buildGeneratorSyncPlan = (table, generatorRows, triggerRows) => {
|
|
|
1066
1066
|
};
|
|
1067
1067
|
};
|
|
1068
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
|
+
|
|
1069
1114
|
export const buildDomainDdlPreview = (domainConfigContent, defaultDbIndex) => {
|
|
1070
1115
|
const buildResult = buildDomainSchemaInternal(domainConfigContent, defaultDbIndex);
|
|
1071
1116
|
const schema = buildResult.schema;
|
|
@@ -1373,6 +1418,11 @@ const buildEntityTable = (entityName, entity, defaultDbIndex) => {
|
|
|
1373
1418
|
table.triggerName = buildObjectName(tableName, "BI");
|
|
1374
1419
|
}
|
|
1375
1420
|
|
|
1421
|
+
if (entity?.appendOnly === true) {
|
|
1422
|
+
table.appendOnly = true;
|
|
1423
|
+
table.appendOnlyTriggerName = buildObjectName(tableName, "NOUPD");
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1376
1426
|
return {
|
|
1377
1427
|
entityName,
|
|
1378
1428
|
tableName,
|
|
@@ -2057,6 +2107,20 @@ const loadGeneratorSyncPlan = async (table, dbIndex, config) => {
|
|
|
2057
2107
|
return buildGeneratorSyncPlan(table, generatorRows, triggerRows);
|
|
2058
2108
|
};
|
|
2059
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
|
+
|
|
2060
2124
|
const toFirebird25CompatibleDdl = (statement) => {
|
|
2061
2125
|
const sanitizedStatement = sanitizeSql(statement);
|
|
2062
2126
|
const notNullMatch = sanitizedStatement.match(
|
|
@@ -2187,6 +2251,22 @@ const applyGeneratorSyncPlan = async (table, generatorSyncPlan, dbIndex, config,
|
|
|
2187
2251
|
return null;
|
|
2188
2252
|
};
|
|
2189
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
|
+
|
|
2190
2270
|
const splitSqlStatements = (sqlBatch) => {
|
|
2191
2271
|
const source = sqlBatch ?? "";
|
|
2192
2272
|
const statements = [];
|
|
@@ -2253,6 +2333,9 @@ const syncTable = async (table, ctx) => {
|
|
|
2253
2333
|
const generatorSyncPlan = await loadGeneratorSyncPlan(table, tableDbIndex, ctx.config);
|
|
2254
2334
|
await applyGeneratorSyncPlan(table, generatorSyncPlan, tableDbIndex, ctx.config, ctx);
|
|
2255
2335
|
|
|
2336
|
+
const createAppendOnlyPlan = await loadAppendOnlySyncPlan(table, tableDbIndex, ctx.config);
|
|
2337
|
+
await applyAppendOnlySyncPlan(table, createAppendOnlyPlan, tableDbIndex, ctx.config, ctx);
|
|
2338
|
+
|
|
2256
2339
|
const latestSnapshot = await loadTableSnapshot(table.name, true, tableDbIndex, ctx.config);
|
|
2257
2340
|
const postCreateSyncPlan = buildTableSyncPlan(table, latestSnapshot, { migrationMode: ctx.migrationMode });
|
|
2258
2341
|
if (postCreateSyncPlan.warnings.length > 0) {
|
|
@@ -2273,6 +2356,9 @@ const syncTable = async (table, ctx) => {
|
|
|
2273
2356
|
const generatorSyncPlan = await loadGeneratorSyncPlan(table, tableDbIndex, ctx.config);
|
|
2274
2357
|
await applyGeneratorSyncPlan(table, generatorSyncPlan, tableDbIndex, ctx.config, ctx);
|
|
2275
2358
|
|
|
2359
|
+
const existingAppendOnlyPlan = await loadAppendOnlySyncPlan(table, tableDbIndex, ctx.config);
|
|
2360
|
+
await applyAppendOnlySyncPlan(table, existingAppendOnlyPlan, tableDbIndex, ctx.config, ctx);
|
|
2361
|
+
|
|
2276
2362
|
if (compareResult.isSame) {
|
|
2277
2363
|
return null;
|
|
2278
2364
|
}
|
|
@@ -237,6 +237,7 @@ export const buildStructureSnapshotFromRows = (input) => {
|
|
|
237
237
|
|
|
238
238
|
export const generateExecuteBlockSql = (schema) => {
|
|
239
239
|
const statements = [];
|
|
240
|
+
let hasAppendOnly = false;
|
|
240
241
|
|
|
241
242
|
for (const table of schema.tables) {
|
|
242
243
|
const createTable = buildCreateTableStatement(table);
|
|
@@ -288,6 +289,18 @@ export const generateExecuteBlockSql = (schema) => {
|
|
|
288
289
|
),
|
|
289
290
|
);
|
|
290
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());
|
|
291
304
|
}
|
|
292
305
|
|
|
293
306
|
return []
|
|
@@ -472,6 +485,30 @@ const resolveIdentityValueExpression = (table, identityColumn, generatorName) =>
|
|
|
472
485
|
return `GEN_ID(${normalizeIdentifier(generatorName)}, 1)`;
|
|
473
486
|
};
|
|
474
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
|
+
|
|
475
512
|
const wrapCreateIfMissing = (statement) => {
|
|
476
513
|
return [
|
|
477
514
|
'EXECUTE BLOCK AS',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import { createLogger, transports, format } from "winston";
|
|
3
|
+
import { queryData } from "../db/db.js";
|
|
3
4
|
|
|
4
5
|
export const CLIENT_ROOM = "clientRoom";
|
|
5
6
|
const MAX_RETRIES = 3;
|
|
@@ -16,14 +17,16 @@ class ClientEmitterService {
|
|
|
16
17
|
constructor() {
|
|
17
18
|
this.ioServerInstance = null;
|
|
18
19
|
this.hubSocket = null;
|
|
20
|
+
this.subdomain = null;
|
|
19
21
|
|
|
20
22
|
// Map<clientInstanceId, Map<msgId, PendingData>>
|
|
21
23
|
this.clientQueues = new Map();
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
init(ioServerInstance, hubSocket) {
|
|
26
|
+
init(ioServerInstance, hubSocket, subdomain) {
|
|
25
27
|
this.ioServerInstance = ioServerInstance;
|
|
26
28
|
this.hubSocket = hubSocket;
|
|
29
|
+
this.subdomain = subdomain;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
/**
|
|
@@ -149,6 +152,7 @@ class ClientEmitterService {
|
|
|
149
152
|
this.ioServerInstance
|
|
150
153
|
.to(CLIENT_ROOM)
|
|
151
154
|
.emit(eventName, wrappedPayload);
|
|
155
|
+
|
|
152
156
|
if (this.hubSocket && this.hubSocket.connected) {
|
|
153
157
|
this.hubSocket.emit("broadcastToClient", {
|
|
154
158
|
eventName,
|
|
@@ -166,6 +170,119 @@ class ClientEmitterService {
|
|
|
166
170
|
this.emitClient(payload.clientInstanceId, payload.eventName, data);
|
|
167
171
|
}
|
|
168
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
|
+
}
|
|
169
286
|
}
|
|
170
287
|
|
|
171
288
|
export const ClientEmitter = new ClientEmitterService();
|
|
@@ -75,6 +75,20 @@ export class CLI_Dispatcher {
|
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
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);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
78
92
|
// --- INTERNAL SYSTEM ROUTING ---
|
|
79
93
|
// ex : this.lib.emitCLI("email:send", data);
|
|
80
94
|
if (this.mode === "monolithic" && this.localBus) {
|
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,
|
|
@@ -87,6 +87,24 @@ export const run = async (apiConfig, scriptIdentifier, scriptData) => {
|
|
|
87
87
|
payload: { eventName, data },
|
|
88
88
|
});
|
|
89
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 },
|
|
106
|
+
});
|
|
107
|
+
};
|
|
90
108
|
};
|
|
91
109
|
|
|
92
110
|
const injectDBMethods = (lib, baseConfig) => {
|
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
|
);
|