biz-a-cli 2.3.80-15375 → 2.3.80-15381
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/app.js +42 -13
- package/bin/directHubEvent.js +81 -3
- package/engine/domain/domainNavigation.js +17 -0
- package/package.json +2 -2
package/bin/app.js
CHANGED
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
import { validateTenancyDeclaration } from "../engine/orm/tenancyDeclaration.js";
|
|
56
56
|
import { logger } from "../logger.js";
|
|
57
57
|
import { findRawColorsInSource } from "../engine/domain/styleColors.js";
|
|
58
|
+
import { resolveMenuLayout } from "../engine/domain/domainNavigation.js";
|
|
58
59
|
|
|
59
60
|
const getKeyFolderPath = () => {
|
|
60
61
|
const scriptPath =
|
|
@@ -922,13 +923,39 @@ const parseApplicationConfigData = (rawConfig) => {
|
|
|
922
923
|
}
|
|
923
924
|
};
|
|
924
925
|
|
|
925
|
-
|
|
926
|
+
/*
|
|
927
|
+
* What the stored APPLICATION_CONFIG contributes to the SYS$APPS metadata: the display name (which
|
|
928
|
+
* also keys the row) and the menu layout the client shell draws.
|
|
929
|
+
*/
|
|
930
|
+
const resolveAppMetadataFromApplicationConfig = async ({
|
|
926
931
|
server,
|
|
927
932
|
apiPort,
|
|
928
933
|
dbIndex,
|
|
929
934
|
sub,
|
|
930
935
|
}) => {
|
|
931
|
-
|
|
936
|
+
const applicationConfig = await readStoredApplicationConfig({
|
|
937
|
+
server,
|
|
938
|
+
apiPort,
|
|
939
|
+
dbIndex,
|
|
940
|
+
sub,
|
|
941
|
+
});
|
|
942
|
+
const displayName = applicationConfig?.metadata?.name;
|
|
943
|
+
const declaredLayout = applicationConfig?.navigation?.layout;
|
|
944
|
+
const menuLayout = resolveMenuLayout(applicationConfig);
|
|
945
|
+
if (declaredLayout != null && menuLayout == null) {
|
|
946
|
+
logger.warn(
|
|
947
|
+
`[Add] navigation.layout ${JSON.stringify(declaredLayout)} is not one of ` +
|
|
948
|
+
`"horizontal" or "vertical"; the client keeps its default menu.`,
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
return {
|
|
952
|
+
displayName: typeof displayName === "string" ? displayName.trim() : "",
|
|
953
|
+
menuLayout,
|
|
954
|
+
};
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
const readStoredApplicationConfig = async ({ server, apiPort, dbIndex, sub }) => {
|
|
958
|
+
/* A PostgreSQL tenant reads SYS$CONFIG directly. This must not silently come back empty: the
|
|
932
959
|
display name keys the SYS$APPS row, so falling back to the random bundle name would insert
|
|
933
960
|
a fresh app row on every publish instead of updating the existing one. */
|
|
934
961
|
if (isPostgresIndex(dbIndex)) {
|
|
@@ -937,15 +964,14 @@ const resolveAppDisplayNameFromApplicationConfig = async ({
|
|
|
937
964
|
buildPostgresApplicationConfigLookupSql(),
|
|
938
965
|
dbIndex,
|
|
939
966
|
);
|
|
940
|
-
|
|
941
|
-
return typeof displayName === "string" ? displayName.trim() : "";
|
|
967
|
+
return parseApplicationConfigData(rows?.[0]?.data);
|
|
942
968
|
} catch {
|
|
943
|
-
return
|
|
969
|
+
return null;
|
|
944
970
|
}
|
|
945
971
|
}
|
|
946
972
|
|
|
947
973
|
if (typeof axios.request !== "function") {
|
|
948
|
-
return
|
|
974
|
+
return null;
|
|
949
975
|
}
|
|
950
976
|
|
|
951
977
|
const baseUrl = "fina/rest/TOrmMethod/%22list%22";
|
|
@@ -989,18 +1015,16 @@ const resolveAppDisplayNameFromApplicationConfig = async ({
|
|
|
989
1015
|
return name === "APPLICATION_CONFIG";
|
|
990
1016
|
});
|
|
991
1017
|
if (!applicationConfigRow) {
|
|
992
|
-
return
|
|
1018
|
+
return null;
|
|
993
1019
|
}
|
|
994
1020
|
|
|
995
|
-
|
|
1021
|
+
return parseApplicationConfigData(
|
|
996
1022
|
applicationConfigRow?.["SYS$CONFIG.DATA"] ??
|
|
997
1023
|
applicationConfigRow?.DATA ??
|
|
998
1024
|
applicationConfigRow?.data,
|
|
999
1025
|
);
|
|
1000
|
-
const displayName = applicationConfig?.metadata?.name;
|
|
1001
|
-
return typeof displayName === "string" ? displayName.trim() : "";
|
|
1002
1026
|
} catch {
|
|
1003
|
-
return
|
|
1027
|
+
return null;
|
|
1004
1028
|
}
|
|
1005
1029
|
};
|
|
1006
1030
|
|
|
@@ -1161,8 +1185,8 @@ async function addApp({
|
|
|
1161
1185
|
: `${server}:${apiPort}/${baseUrl}`;
|
|
1162
1186
|
|
|
1163
1187
|
const headers = { "Content-Type": "text/plain" };
|
|
1164
|
-
const appDisplayName =
|
|
1165
|
-
await
|
|
1188
|
+
const { displayName: appDisplayName, menuLayout } =
|
|
1189
|
+
await resolveAppMetadataFromApplicationConfig({
|
|
1166
1190
|
server,
|
|
1167
1191
|
apiPort,
|
|
1168
1192
|
dbIndex,
|
|
@@ -1172,6 +1196,11 @@ async function addApp({
|
|
|
1172
1196
|
if (useApplicationConfigName && appDisplayName.length > 0) {
|
|
1173
1197
|
keys.metadata["name"] = appDisplayName;
|
|
1174
1198
|
}
|
|
1199
|
+
/* navigation.layout -> how the client shell draws this app's menu. Only a value it
|
|
1200
|
+
understands is copied; without one the metadata stays exactly as before. */
|
|
1201
|
+
if (useApplicationConfigName && menuLayout) {
|
|
1202
|
+
keys.metadata["menuLayout"] = menuLayout;
|
|
1203
|
+
}
|
|
1175
1204
|
const uploadAppName = normalizeAppName(
|
|
1176
1205
|
(useApplicationConfigName ? appDisplayName : "") || bundleName,
|
|
1177
1206
|
);
|
package/bin/directHubEvent.js
CHANGED
|
@@ -6,6 +6,8 @@ import { CLIENT_ROOM } from "../message-broker/clientEmitter.js";
|
|
|
6
6
|
import { ClientEmitter } from "../message-broker/clientEmitter.js";
|
|
7
7
|
import { joinExtRooms } from "../engine/ext/esb.js";
|
|
8
8
|
import { extClientListener } from "../engine/ext/socketListener.js";
|
|
9
|
+
import { io as ioClient } from "socket.io-client";
|
|
10
|
+
|
|
9
11
|
import { prefixedLogger } from "../logger.js";
|
|
10
12
|
import { APP_URL, TEST_URL, ADMIN_URL, TEST_ADMIN_URL } from "../envs/hosts.js";
|
|
11
13
|
|
|
@@ -14,28 +16,99 @@ const logger = prefixedLogger("[DirectHub]");
|
|
|
14
16
|
export async function localhostTunnel(port, notifier) {
|
|
15
17
|
let tunnelUrl = "";
|
|
16
18
|
let qt = QuickTunnel.quick("127.0.0.1:" + port);
|
|
19
|
+
let isReconnecting = false;
|
|
20
|
+
let loopbackSocket = null;
|
|
21
|
+
|
|
22
|
+
function destroyLoopback() {
|
|
23
|
+
if (loopbackSocket) {
|
|
24
|
+
loopbackSocket.disconnect();
|
|
25
|
+
loopbackSocket.close();
|
|
26
|
+
loopbackSocket = null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const gracefulProcessShutdown = () => {
|
|
31
|
+
destroyLoopback();
|
|
32
|
+
if (qt && typeof qt.stop === "function") {
|
|
33
|
+
try {
|
|
34
|
+
qt.stop();
|
|
35
|
+
} catch (e) {}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
process.once("SIGINT", gracefulProcessShutdown);
|
|
39
|
+
process.once("SIGTERM", gracefulProcessShutdown);
|
|
40
|
+
|
|
17
41
|
function reconnectTunnel() {
|
|
42
|
+
process.off("SIGINT", gracefulProcessShutdown);
|
|
43
|
+
process.off("SIGTERM", gracefulProcessShutdown);
|
|
44
|
+
destroyLoopback();
|
|
45
|
+
|
|
46
|
+
if (isReconnecting) return;
|
|
47
|
+
isReconnecting = true;
|
|
48
|
+
|
|
49
|
+
if (qt && typeof qt.stop === "function") {
|
|
50
|
+
try {
|
|
51
|
+
qt.stop();
|
|
52
|
+
} catch (e) {}
|
|
53
|
+
}
|
|
54
|
+
|
|
18
55
|
setTimeout(() => {
|
|
19
56
|
qt = localhostTunnel(port, notifier);
|
|
20
57
|
}, RECONNECT_SOCKET_DELAY);
|
|
21
58
|
}
|
|
59
|
+
|
|
60
|
+
function startDirectHubHeartbeat() {
|
|
61
|
+
if (loopbackSocket && loopbackSocket.connected) return;
|
|
62
|
+
|
|
63
|
+
if (!tunnelUrl || !tunnelUrl.includes("trycloudflare.com")) {
|
|
64
|
+
logger.warn(`Invalid remote URL detected. Heartbeat aborted.`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
destroyLoopback();
|
|
69
|
+
|
|
70
|
+
loopbackSocket = ioClient(tunnelUrl, {
|
|
71
|
+
query: { isLoopback: true },
|
|
72
|
+
transports: ["websocket"],
|
|
73
|
+
reconnectionDelay: 5000,
|
|
74
|
+
reconnectionAttempts: 3,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
loopbackSocket.on("connect", () => {
|
|
78
|
+
logger.debug(
|
|
79
|
+
`Heartbeat established via public internet. Cloudflare idle-drop prevented.`,
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
loopbackSocket.io.on("reconnect_failed", () => {
|
|
84
|
+
logger.warn(
|
|
85
|
+
`Heartbeat failed to reach Edge. Ghost tunnel confirmed. Terminating...`,
|
|
86
|
+
);
|
|
87
|
+
notifier("");
|
|
88
|
+
destroyLoopback();
|
|
89
|
+
if (qt && typeof qt.stop === "function")
|
|
90
|
+
try {
|
|
91
|
+
qt.stop();
|
|
92
|
+
} catch (e) {}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
22
96
|
qt.on("url", (qtUrl) => {
|
|
23
97
|
tunnelUrl = qtUrl;
|
|
24
98
|
})
|
|
25
99
|
.on("connected", (conn) => {
|
|
26
100
|
notifier(tunnelUrl);
|
|
27
101
|
logger.info(`Connected to Direct Hub at public URL ${tunnelUrl}`);
|
|
102
|
+
startDirectHubHeartbeat();
|
|
28
103
|
})
|
|
29
104
|
.on("disconnected", (conn) => {
|
|
30
105
|
notifier("");
|
|
31
|
-
qt.stop();
|
|
32
106
|
logger.info(
|
|
33
107
|
`Disconnected from edge node. Awaiting native reconnect...`,
|
|
34
108
|
);
|
|
35
109
|
})
|
|
36
110
|
.on("error", (err) => {
|
|
37
111
|
notifier("");
|
|
38
|
-
qt.stop();
|
|
39
112
|
logger.error(
|
|
40
113
|
`Tunnel error: ${err.message || err}. Awaiting native reconnect...`,
|
|
41
114
|
);
|
|
@@ -91,7 +164,10 @@ export function directHubEvent(serverSocket, argv) {
|
|
|
91
164
|
if (clientInstanceId) {
|
|
92
165
|
clientSocket.join(clientInstanceId);
|
|
93
166
|
clientSocket.on("ESB_ACK", (msgId) => {
|
|
94
|
-
ClientEmitter.acknowledgeMessage(
|
|
167
|
+
ClientEmitter.acknowledgeMessage(
|
|
168
|
+
msgId,
|
|
169
|
+
clientInstanceId,
|
|
170
|
+
);
|
|
95
171
|
});
|
|
96
172
|
}
|
|
97
173
|
|
|
@@ -108,6 +184,8 @@ export function directHubEvent(serverSocket, argv) {
|
|
|
108
184
|
} else if (clientSocket.handshake.query.isDeploy) {
|
|
109
185
|
setConnectListeners(clientSocket);
|
|
110
186
|
deploymentListenerForVSCode(clientSocket, argv);
|
|
187
|
+
} else if (clientSocket.handshake.query.isLoopback) {
|
|
188
|
+
setConnectListeners(clientSocket);
|
|
111
189
|
} else {
|
|
112
190
|
clientSocket.disconnect();
|
|
113
191
|
}
|
|
@@ -18,6 +18,23 @@
|
|
|
18
18
|
/* The route shape the client's menu component renders, identical to a hand-declared leaf. */
|
|
19
19
|
const linkFor = (entry) => ["./form", String(entry?.entryPoint ?? "").trim(), null];
|
|
20
20
|
|
|
21
|
+
/* The menu layouts the client shell can draw. */
|
|
22
|
+
const MENU_LAYOUTS = ["horizontal", "vertical"];
|
|
23
|
+
|
|
24
|
+
/*
|
|
25
|
+
* How the client shell draws this app's menu: `navigation.layout` in application.js.
|
|
26
|
+
*
|
|
27
|
+
* It reaches the browser as SYS$APPS.METADATA.menuLayout, beside the display name. Answers null when
|
|
28
|
+
* nothing usable is declared, so an app that never mentions it keeps its metadata exactly as it was
|
|
29
|
+
* and the client keeps the horizontal bar.
|
|
30
|
+
*/
|
|
31
|
+
export const resolveMenuLayout = (applicationConfig) => {
|
|
32
|
+
const raw = applicationConfig?.navigation?.layout;
|
|
33
|
+
if (typeof raw !== "string") return null;
|
|
34
|
+
const layout = raw.trim().toLowerCase();
|
|
35
|
+
return MENU_LAYOUTS.includes(layout) ? layout : null;
|
|
36
|
+
};
|
|
37
|
+
|
|
21
38
|
/*
|
|
22
39
|
* Which of a domain's declared entries are live right now.
|
|
23
40
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "biz-a-cli",
|
|
3
|
-
"version": "2.3.80-
|
|
3
|
+
"version": "2.3.80-15381",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "bin/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"axios": "^1.7.8",
|
|
48
|
-
"cloudflared": "^0.
|
|
48
|
+
"cloudflared": "^0.7.2",
|
|
49
49
|
"compression": "^1.7.5",
|
|
50
50
|
"cors": "^2.8.5",
|
|
51
51
|
"dayjs": "^1.11.10",
|