biz-a-cli 2.3.80-15296 → 2.3.80-15317

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.
Files changed (74) hide show
  1. package/bin/app.js +154 -17
  2. package/bin/deleteApp.js +4 -3
  3. package/bin/directHubEvent.js +25 -25
  4. package/bin/hub.js +404 -352
  5. package/bin/hubEvent.js +713 -505
  6. package/bin/index.js +5 -4
  7. package/bin/migrate.js +289 -173
  8. package/bin/proxy.js +26 -25
  9. package/bin/script.js +125 -124
  10. package/bin/tunnelTarget.js +41 -0
  11. package/bin/uploadApp.js +6 -5
  12. package/bin/watcher.js +49 -63
  13. package/callbackController.js +65 -64
  14. package/config/databases.example.json +42 -0
  15. package/config/databases.json +20 -0
  16. package/config/ext-oidc.json +7 -0
  17. package/config/secrets.json +21 -0
  18. package/db/db.js +288 -43
  19. package/engine/bpm/bpm-agent.js +467 -385
  20. package/engine/bpm/routes.js +66 -61
  21. package/engine/bpm/workflow-runtime.js +1676 -1626
  22. package/engine/bpm/workflow.js +290 -283
  23. package/engine/domain/appConfig.js +294 -54
  24. package/engine/domain/applicationConfig.js +7 -7
  25. package/engine/domain/dialect.js +323 -0
  26. package/engine/domain/entityConfigLoader.js +100 -0
  27. package/engine/domain/firebird-ddl.js +14 -1
  28. package/engine/domain/postgres-ddl.js +565 -0
  29. package/engine/ext/token.js +8 -10
  30. package/engine/orm/apiRoute.js +486 -0
  31. package/engine/orm/changeInference.js +213 -0
  32. package/engine/orm/concurrency.js +132 -0
  33. package/engine/orm/connection.js +245 -0
  34. package/engine/orm/crudQuery.js +198 -0
  35. package/engine/orm/dataSnapShape.js +107 -0
  36. package/engine/orm/dialect.js +41 -0
  37. package/engine/orm/entityConfig.js +144 -0
  38. package/engine/orm/executeBlock.js +493 -0
  39. package/engine/orm/executor.js +285 -0
  40. package/engine/orm/finaAuth.js +0 -0
  41. package/engine/orm/finaEndpoint.js +166 -0
  42. package/engine/orm/imgQuery.js +82 -0
  43. package/engine/orm/index.js +137 -0
  44. package/engine/orm/joinChain.js +143 -0
  45. package/engine/orm/listQuery.js +164 -0
  46. package/engine/orm/masterDetail.js +395 -0
  47. package/engine/orm/registry.js +92 -0
  48. package/engine/orm/registryClient.js +63 -0
  49. package/engine/orm/userAdmin.js +98 -0
  50. package/logger.js +122 -0
  51. package/mailController.js +19 -18
  52. package/message-broker/agent.js +63 -62
  53. package/message-broker/clientEmitter.js +304 -310
  54. package/message-broker/dispatcher.js +161 -158
  55. package/message-broker/orchestrator.js +2 -2
  56. package/migrations/1788527682689__privileges.sql +103 -0
  57. package/migrations/postgres/1777727800000__config.sql +21 -0
  58. package/migrations/postgres/1777727810000__sys_runtime.sql +124 -0
  59. package/migrations/postgres/1777727873750__apps.sql +30 -0
  60. package/migrations/postgres/1777727892577__bpm.sql +242 -0
  61. package/migrations/postgres/1787137466726__ext.sql +74 -0
  62. package/migrations/postgres/1787201361442__ext_cli.sql +12 -0
  63. package/migrations/postgres/1788527682689__privileges.sql +68 -0
  64. package/package.json +6 -1
  65. package/proxyController.js +83 -82
  66. package/scheduler/configController.js +116 -115
  67. package/scheduler/datalib.js +355 -364
  68. package/scheduler/timer.js +88 -87
  69. package/scheduler/watcherController.js +94 -93
  70. package/security/internalPrincipal.js +134 -0
  71. package/security/secretProvider.js +6 -1
  72. package/security/tokenFormat.js +38 -0
  73. package/worker/cliScriptWorker.js +205 -186
  74. package/worker/cliWorkerPool.js +4 -1
package/bin/app.js CHANGED
@@ -18,12 +18,23 @@ import { spawn } from "node:child_process";
18
18
  import { newMigration, runMigrations } from "./migrate.js";
19
19
  import {
20
20
  APP_CONFIG_NAME,
21
+ buildEnsureConfigTableSql,
22
+ buildPostgresConfigUpsertSql,
23
+ buildPostgresApplicationConfigLookupSql,
24
+ buildPostgresSetAppSql,
21
25
  extractConfigVersion,
22
26
  getChunkedUpsertBlocks,
23
27
  getUpsertBlock,
24
28
  isSemanticVersion,
25
29
  parseConfigForVersion,
26
30
  } from "../engine/domain/appConfig.js";
31
+ import {
32
+ isPostgresIndex,
33
+ execPostgres,
34
+ primeRegistry,
35
+ } from "../engine/domain/dialect.js";
36
+ import { addUser } from "../engine/orm/userAdmin.js";
37
+ import { logger } from "../logger.js";
27
38
 
28
39
  const getKeyFolderPath = () => {
29
40
  const scriptPath =
@@ -511,6 +522,11 @@ async function uploadConfigs({
511
522
  publish = false,
512
523
  forceFull = false,
513
524
  }) {
525
+ /* Phase B2: ask biz-a for this tenant's database coordinates before anything reads the
526
+ dialect. A deployed CLI has no config/databases.json, so without this the registry
527
+ indexes would look like Firebird. No-op when --sub is absent or biz-a is unreachable. */
528
+ await primeRegistry({ subdomain: sub, serverLink: env.BIZA_SERVER_LINK });
529
+
514
530
  const { application, domains } = collectConfigUploads({
515
531
  workingDir,
516
532
  fileNames,
@@ -523,11 +539,24 @@ async function uploadConfigs({
523
539
  };
524
540
 
525
541
  const saveOne = async ({ name, version, content }) => {
542
+ /* A PostgreSQL index writes straight to its pool. No chunking: the block splitting below
543
+ exists only because the FINA HTTP body has a size limit, which a direct connection does
544
+ not. */
545
+ if (isPostgresIndex(dbIndex)) {
546
+ await execPostgres(buildEnsureConfigTableSql(), dbIndex);
547
+ await execPostgres(
548
+ buildPostgresConfigUpsertSql(name, version, content, false),
549
+ dbIndex,
550
+ );
551
+ logger.info(`saved ${name}@${version} (postgres db${dbIndex})`);
552
+ return;
553
+ }
554
+
526
555
  const blocks = resolveUpsertBlocks(name, version, content, dbIndex);
527
556
  for (const block of blocks) {
528
557
  await executeConfigBlock(block, { server, apiPort, dbIndex });
529
558
  }
530
- console.log(
559
+ logger.info(
531
560
  `saved ${name}@${version}${blocks.length > 1 ? ` (${blocks.length} chunks)` : ""}`,
532
561
  );
533
562
  };
@@ -579,7 +608,7 @@ async function uploadConfigs({
579
608
  `Publish ${domain.name}@${domain.version} failed: ${result?.error ?? "no error message from server"}`,
580
609
  );
581
610
  }
582
- console.log(`published ${domain.name}@${domain.version}`);
611
+ logger.info(`published ${domain.name}@${domain.version}`);
583
612
  }
584
613
 
585
614
  if (application) {
@@ -608,7 +637,7 @@ async function uploadConfigs({
608
637
  `Publish ${application.name}@${application.version} failed: ${result?.error ?? "no error message from server"}`,
609
638
  );
610
639
  }
611
- console.log(`published ${application.name}@${application.version}`);
640
+ logger.info(`published ${application.name}@${application.version}`);
612
641
  } finally {
613
642
  process.chdir(previousCwd);
614
643
  fs.rmSync(scratchDir, { force: true, recursive: true });
@@ -657,6 +686,22 @@ const resolveAppDisplayNameFromApplicationConfig = async ({
657
686
  dbIndex,
658
687
  sub,
659
688
  }) => {
689
+ /* A PostgreSQL tenant reads SYS$CONFIG directly. This must not silently return "": the
690
+ display name keys the SYS$APPS row, so falling back to the random bundle name would insert
691
+ a fresh app row on every publish instead of updating the existing one. */
692
+ if (isPostgresIndex(dbIndex)) {
693
+ try {
694
+ const rows = await execPostgres(
695
+ buildPostgresApplicationConfigLookupSql(),
696
+ dbIndex,
697
+ );
698
+ const displayName = parseApplicationConfigData(rows?.[0]?.data)?.metadata?.name;
699
+ return typeof displayName === "string" ? displayName.trim() : "";
700
+ } catch {
701
+ return "";
702
+ }
703
+ }
704
+
660
705
  if (typeof axios.request !== "function") {
661
706
  return "";
662
707
  }
@@ -787,7 +832,7 @@ async function addApp({
787
832
  const keys = await prepareKeys();
788
833
  if (!keys) {
789
834
  const msg = "Can not prepare encryption keys";
790
- console.error(msg);
835
+ logger.error(msg);
791
836
  return { success: false, error: msg };
792
837
  }
793
838
 
@@ -825,7 +870,7 @@ async function addApp({
825
870
  }
826
871
  const bundleFile = `${rootFolder}${bundleName}.tgz`;
827
872
  compressIt(bundleFile, bundleFolder);
828
- console.log(
873
+ logger.info(
829
874
  `Finished packing ${processedFile} files into "${bundleFile}" (${((performance.now() - bundlingStart) / 1000).toFixed(2)}s)`,
830
875
  );
831
876
 
@@ -853,29 +898,44 @@ async function addApp({
853
898
  const uploadAppName = normalizeAppName(
854
899
  (useApplicationConfigName ? appDisplayName : "") || bundleName,
855
900
  );
901
+ /* SYS$APPS is a Biz-A table, not a FINA one, so a PostgreSQL tenant is written
902
+ directly — no FINA server in the path at all. On Firebird the FINA stored
903
+ procedure TOrmMethod/"setApp" still owns the write. */
904
+ if (isPostgresIndex(dbIndex)) {
905
+ await execPostgres(
906
+ buildPostgresSetAppSql(uploadAppName, data, keys.metadata),
907
+ dbIndex,
908
+ );
909
+ logger.info(
910
+ `Finished uploading "${bundleFile}" to postgres db${dbIndex} (${((performance.now() - uploadingStart) / 1000).toFixed(2)}s)`,
911
+ );
912
+ fs.rmSync(rootFolder, { force: true, recursive: true });
913
+ return { success: true, data: { appName: uploadAppName } };
914
+ }
915
+
856
916
  const param = {
857
917
  _parameters: [dbIndex, uploadAppName, data, keys.metadata],
858
918
  };
859
919
 
860
920
  const res = await axios.post(url, param, { headers: headers });
861
921
  if (res.data.success) {
862
- console.log(
922
+ logger.info(
863
923
  `Finished uploading "${bundleFile}" (${((performance.now() - uploadingStart) / 1000).toFixed(2)}s)`,
864
924
  );
865
925
  fs.rmSync(rootFolder, { force: true, recursive: true });
866
926
  return { success: true, data: res.data };
867
927
  }
868
928
 
869
- console.error(res.data.error);
929
+ logger.error(res.data.error);
870
930
  return { success: false, error: res.data.error, data: res.data };
871
931
  }
872
932
 
873
933
  const msg = "Nothing to upload. Please recheck your app folder.";
874
- console.error(msg);
934
+ logger.error(msg);
875
935
  return { success: false, error: msg };
876
936
  } catch (e) {
877
937
  const errMsg = e.response?.data ? e.response.data : e;
878
- console.error(errMsg);
938
+ logger.error(errMsg);
879
939
  return { success: false, error: errMsg };
880
940
  } finally {
881
941
  process.chdir(oldCwd);
@@ -914,9 +974,9 @@ async function runUnitTests(options) {
914
974
  });
915
975
 
916
976
  child.on("close", async (code) => {
917
- console.log("====================");
918
- console.log(output);
919
- console.log("====================");
977
+ logger.info("====================");
978
+ logger.info(output);
979
+ logger.info("====================");
920
980
  const noTestsFound = output.includes("No tests found");
921
981
  const missingJest =
922
982
  output.includes("missing packages") && output.includes("jest");
@@ -939,7 +999,7 @@ async function runUnitTests(options) {
939
999
  forceFull: options.forceFull,
940
1000
  });
941
1001
  } else {
942
- console.error("Biz-A Add aborted");
1002
+ logger.error("Biz-A Add aborted");
943
1003
  }
944
1004
  resolve();
945
1005
  });
@@ -999,7 +1059,7 @@ const buildCli = () =>
999
1059
  });
1000
1060
  if (res.data?.success) {
1001
1061
  if (deleteApps == "") {
1002
- console.log("All apps removed");
1062
+ logger.info("All apps removed");
1003
1063
  } else {
1004
1064
  const failedList =
1005
1065
  res.data._f &&
@@ -1012,18 +1072,18 @@ const buildCli = () =>
1012
1072
  : [];
1013
1073
  const removeList = deleteApps.split(",");
1014
1074
  removeList.forEach((app) => {
1015
- console.log(
1075
+ logger.info(
1016
1076
  `${app} ${failedList.indexOf(app) == -1 ? "removed" : "not found"}`,
1017
1077
  );
1018
1078
  });
1019
1079
  }
1020
1080
  } else {
1021
- console.error(res.data.error);
1081
+ logger.error(res.data.error);
1022
1082
  }
1023
1083
  return res;
1024
1084
  } catch (e) {
1025
1085
  const errMsg = e.response?.data ? e.response.data : e;
1026
- console.error(errMsg);
1086
+ logger.error(errMsg);
1027
1087
  return errMsg;
1028
1088
  }
1029
1089
  })();
@@ -1043,6 +1103,83 @@ const buildCli = () =>
1043
1103
  },
1044
1104
  (commandOptions) => newMigration(commandOptions),
1045
1105
  )
1106
+ .command(
1107
+ "user:add",
1108
+ "Create a platform user in SYS$USERS on a PostgreSQL tenant (defaults: supervisor/supervisor)",
1109
+ (yargs) =>
1110
+ yargs
1111
+ .option("u", {
1112
+ alias: "username",
1113
+ describe: "User name (stored upper-cased, as the login expects)",
1114
+ type: "string",
1115
+ default: "supervisor",
1116
+ })
1117
+ .option("w", {
1118
+ alias: "password",
1119
+ describe: "Password. Hashed with scrypt before it is stored",
1120
+ type: "string",
1121
+ default: "supervisor",
1122
+ })
1123
+ .option("fullname", {
1124
+ describe: "Display name (defaults to the user name)",
1125
+ type: "string",
1126
+ })
1127
+ .option("userlevel", {
1128
+ describe: "User level. Anything below 0 cannot log in",
1129
+ type: "number",
1130
+ default: 9,
1131
+ })
1132
+ .option("replace", {
1133
+ describe:
1134
+ "Reset the password if the user already exists. Without it an existing user is left untouched",
1135
+ type: "boolean",
1136
+ default: false,
1137
+ })
1138
+ .option("admin", {
1139
+ describe:
1140
+ "Grant admin (SYS$ADMINISTRATOR). The FIRST user is granted automatically - without one, nobody can reach the admin screens or publish",
1141
+ type: "boolean",
1142
+ default: false,
1143
+ }),
1144
+ async (commandOptions) => {
1145
+ const dbIndex = Number(commandOptions.dbIndex);
1146
+ try {
1147
+ await primeRegistry({
1148
+ subdomain: commandOptions.sub,
1149
+ serverLink: env.BIZA_SERVER_LINK,
1150
+ });
1151
+ const result = await addUser(
1152
+ {
1153
+ username: commandOptions.username,
1154
+ password: commandOptions.password,
1155
+ fullname: commandOptions.fullname,
1156
+ userlevel: commandOptions.userlevel,
1157
+ replace: commandOptions.replace,
1158
+ admin: commandOptions.admin,
1159
+ },
1160
+ {
1161
+ exec: execPostgres,
1162
+ dbIndex,
1163
+ isPostgres: isPostgresIndex(dbIndex),
1164
+ },
1165
+ );
1166
+ if (result.status === "exists") {
1167
+ logger.info(
1168
+ `user ${result.username} already exists in db${dbIndex} - password left untouched. Use --replace to reset it.` +
1169
+ (result.admin ? " Granted admin." : ""),
1170
+ );
1171
+ return;
1172
+ }
1173
+ logger.info(
1174
+ `user ${result.username} ${result.status} in db${dbIndex}` +
1175
+ (result.admin ? " and granted admin" : ""),
1176
+ );
1177
+ } catch (error) {
1178
+ logger.error(`user:add failed: ${error?.message ?? error}`);
1179
+ process.exitCode = 1;
1180
+ }
1181
+ },
1182
+ )
1046
1183
  .command(
1047
1184
  "migrate:run",
1048
1185
  "Run pending DDL migrations from the /migrations folder",
package/bin/deleteApp.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import yargs from "yargs";
4
4
  import axios from "axios";
5
+ import { logger } from "../logger.js";
5
6
  import { env } from "../envs/env.js"
6
7
 
7
8
  const options = yargs(process.argv.slice(2))
@@ -52,12 +53,12 @@ const deleteApp = async () => {
52
53
  const param = { _parameters: [options.dbindex, deleteApps] }
53
54
  const res = await axios.post(url, param, { headers: headers });
54
55
  if (res.data.success) {
55
- console.log(`${deleteApps=='' ? 'All' : deleteApps} apps removed`)
56
+ logger.info(`${deleteApps=='' ? 'All' : deleteApps} apps removed`)
56
57
  } else {
57
- console.error(res.data.error)
58
+ logger.error(res.data.error)
58
59
  }
59
60
  } catch (e) {
60
- console.error(e.response?.data ? e.response.data : e)
61
+ logger.error(e.response?.data ? e.response.data : e)
61
62
  }
62
63
  }
63
64
 
@@ -6,6 +6,9 @@ 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 { prefixedLogger } from "../logger.js";
10
+
11
+ const logger = prefixedLogger("[DirectHub]");
9
12
 
10
13
  export async function localhostTunnel(port, notifier) {
11
14
  let tunnelUrl = "";
@@ -16,55 +19,52 @@ export async function localhostTunnel(port, notifier) {
16
19
  }, RECONNECT_SOCKET_DELAY);
17
20
  }
18
21
  qt.on("url", (qtUrl) => {
19
- // console.log('qt url', qtUrl)
20
22
  tunnelUrl = qtUrl;
21
23
  })
22
24
  .on("connected", (conn) => {
23
- // console.log('qt connect', conn)
24
25
  notifier(tunnelUrl);
25
- console.log(
26
- `${new Date()}: Connected to Direct Hub at public URL ${tunnelUrl}`,
27
- );
26
+ logger.info(`Connected to Direct Hub at public URL ${tunnelUrl}`);
28
27
  })
29
28
  .on("disconnected", (conn) => {
30
- // console.log('qt disconnect', conn)
31
29
  notifier("");
32
30
  qt.stop();
31
+ logger.info(
32
+ `Disconnected from edge node. Awaiting native reconnect...`,
33
+ );
33
34
  })
34
35
  .on("error", (err) => {
35
- // console.log('qt err', err)
36
36
  notifier("");
37
37
  qt.stop();
38
+ logger.error(
39
+ `Tunnel error: ${err.message || err}. Awaiting native reconnect...`,
40
+ );
38
41
  })
39
42
  .on("exit", (code) => {
40
- // console.log('qt exit', code)
41
43
  notifier("");
42
- console.log(
43
- `${new Date()}: Direct Hub is not available, will try to reinitiate it in ${RECONNECT_SOCKET_DELAY / 1000} seconds. Error code : `,
44
+ logger.error(
45
+ `Direct Hub is not available, will try to reinitiate it in ${RECONNECT_SOCKET_DELAY / 1000} seconds. Error code : `,
44
46
  code,
45
47
  );
46
48
  reconnectTunnel();
47
- // })
48
- // on('stdout', (data)=>{
49
- // console.log('qt stdout', data)
50
- // })
51
- // on('stderr', (data)=>{
52
- // console.log('qt stderr', data)
49
+ })
50
+ .on("stdout", (data) => {
51
+ logger.debug(`stdout`, data);
52
+ })
53
+ .on("stderr", (data) => {
54
+ logger.debug(`stderr`, data);
53
55
  });
56
+
54
57
  return qt;
55
58
  }
56
59
 
57
60
  export function directHubEvent(serverSocket, argv) {
58
61
  serverSocket.on("connection", (clientSocket) => {
59
62
  const setConnectListeners = (sock) => {
60
- // shall not log in production mode
61
- if (process.env.NODE_ENV !== "production") {
62
- const id = `<${sock.handshake?.query?.isClient ? `Client` : sock.handshake?.query?.isDeploy ? "Deployer" : "Anonymous"}> ${sock.id}`;
63
- console.log(id, `connected`);
64
- sock.on("disconnect", (reason) => {
65
- console.log(id, `disconnected. Reason : ${reason}`);
66
- });
67
- }
63
+ const id = `<${sock.handshake?.query?.isClient ? `Client` : sock.handshake?.query?.isDeploy ? "Deployer" : sock.handshake?.query?.isLoopback ? "DirectHub Heartbeat" : "Anonymous"}> ${sock.id}`;
64
+ logger.debug(id, `connected`);
65
+ sock.on("disconnect", (reason) => {
66
+ logger.debug(id, `disconnected. Reason : ${reason}`);
67
+ });
68
68
  };
69
69
 
70
70
  if (clientSocket.handshake.query.isClient) {
@@ -81,7 +81,7 @@ export function directHubEvent(serverSocket, argv) {
81
81
  joinExtRooms(clientSocket, extToken, { slug: extSlug });
82
82
  } else {
83
83
  // INTERNAL client — full listener + existing rooms, unchanged.
84
- clientListener(clientSocket, argv);
84
+ clientListener(clientSocket, argv, "direct");
85
85
  clientSocket.join(CLIENT_ROOM);
86
86
 
87
87
  // isolated Client in a room using clientInstanceId