biz-a-cli 2.3.80-15296 → 2.3.80-15326

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 (102) hide show
  1. package/bin/app.js +358 -22
  2. package/bin/deleteApp.js +4 -3
  3. package/bin/directHubEvent.js +38 -28
  4. package/bin/hub.js +501 -352
  5. package/bin/hubEvent.js +833 -505
  6. package/bin/index.js +5 -4
  7. package/bin/migrate.js +292 -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/apiUrl.js +58 -0
  19. package/db/db.js +396 -43
  20. package/engine/bpm/bpm-agent.js +467 -385
  21. package/engine/bpm/routes.js +66 -61
  22. package/engine/bpm/workflow-runtime.js +1676 -1626
  23. package/engine/bpm/workflow.js +290 -283
  24. package/engine/domain/appConfig.js +521 -92
  25. package/engine/domain/applicationConfig.js +7 -7
  26. package/engine/domain/dialect.js +323 -0
  27. package/engine/domain/entityConfigLoader.js +100 -0
  28. package/engine/domain/entityConfigPrimer.js +97 -0
  29. package/engine/domain/firebird-ddl.js +41 -14
  30. package/engine/domain/naming.js +47 -0
  31. package/engine/domain/postgres-ddl.js +625 -0
  32. package/engine/domain/publishedAdditions.js +69 -0
  33. package/engine/domain/system.js +3 -0
  34. package/engine/ext/channel.js +3 -0
  35. package/engine/ext/dispatch.js +19 -2
  36. package/engine/ext/live.js +19 -0
  37. package/engine/ext/oidc.js +1 -1
  38. package/engine/ext/routes.js +2 -1
  39. package/engine/ext/token.js +9 -11
  40. package/engine/orm/apiRoute.js +792 -0
  41. package/engine/orm/changeInference.js +261 -0
  42. package/engine/orm/concurrency.js +132 -0
  43. package/engine/orm/connection.js +294 -0
  44. package/engine/orm/crudQuery.js +288 -0
  45. package/engine/orm/dataSnapShape.js +127 -0
  46. package/engine/orm/dialect.js +41 -0
  47. package/engine/orm/entityConfig.js +643 -0
  48. package/engine/orm/executeBlock.js +728 -0
  49. package/engine/orm/executor.js +291 -0
  50. package/engine/orm/finaAuth.js +0 -0
  51. package/engine/orm/finaEndpoint.js +230 -0
  52. package/engine/orm/imgQuery.js +82 -0
  53. package/engine/orm/index.js +255 -0
  54. package/engine/orm/joinChain.js +143 -0
  55. package/engine/orm/listQuery.js +224 -0
  56. package/engine/orm/masterDetail.js +528 -0
  57. package/engine/orm/registry.js +92 -0
  58. package/engine/orm/registryClient.js +63 -0
  59. package/engine/orm/requestScope.js +157 -0
  60. package/engine/orm/rlsApplier.js +74 -0
  61. package/engine/orm/rlsPolicy.js +370 -0
  62. package/engine/orm/sessionStore.js +337 -0
  63. package/engine/orm/tenancyDeclaration.js +268 -0
  64. package/engine/orm/tenantApi.js +416 -0
  65. package/engine/orm/tenantContext.js +216 -0
  66. package/engine/orm/tenantResolution.js +426 -0
  67. package/engine/orm/tenantScope.js +82 -0
  68. package/engine/orm/userAdmin.js +98 -0
  69. package/envs/env.dev.js +7 -2
  70. package/envs/env.js +7 -2
  71. package/envs/hosts.js +36 -0
  72. package/envs/serverEndpoints.js +50 -0
  73. package/logger.js +122 -0
  74. package/mailController.js +19 -18
  75. package/message-broker/agent.js +63 -62
  76. package/message-broker/clientEmitter.js +304 -310
  77. package/message-broker/dispatcher.js +161 -158
  78. package/message-broker/orchestrator.js +2 -2
  79. package/migrations/1788527682689__privileges.sql +103 -0
  80. package/migrations/postgres/1777727800000__config.sql +21 -0
  81. package/migrations/postgres/1777727810000__sys_runtime.sql +124 -0
  82. package/migrations/postgres/1777727873750__apps.sql +30 -0
  83. package/migrations/postgres/1777727892577__bpm.sql +242 -0
  84. package/migrations/postgres/1787137466726__ext.sql +74 -0
  85. package/migrations/postgres/1787201361442__ext_cli.sql +12 -0
  86. package/migrations/postgres/1788527682689__privileges.sql +68 -0
  87. package/migrations/postgres/1789040354328__tenancy.sql +33 -0
  88. package/migrations/postgres/1789200000000__rls_role.sql +46 -0
  89. package/migrations/postgres/1789350000000__user_tenant.sql +111 -0
  90. package/migrations/postgres/1789500000000__session.sql +73 -0
  91. package/package.json +6 -1
  92. package/proxyController.js +83 -82
  93. package/readme.md +5 -5
  94. package/scheduler/configController.js +116 -115
  95. package/scheduler/datalib.js +355 -364
  96. package/scheduler/timer.js +88 -87
  97. package/scheduler/watcherController.js +94 -93
  98. package/security/internalPrincipal.js +201 -0
  99. package/security/secretProvider.js +6 -1
  100. package/security/tokenFormat.js +38 -0
  101. package/worker/cliScriptWorker.js +234 -186
  102. package/worker/cliWorkerPool.js +4 -1
package/bin/app.js CHANGED
@@ -13,17 +13,39 @@ import {
13
13
  import path, { basename } from "node:path";
14
14
  import os from "node:os";
15
15
  import { env } from "../envs/env.js";
16
+ import {
17
+ SERVER_MODES,
18
+ resolveServerLink,
19
+ } from "../envs/serverEndpoints.js";
16
20
  import { prepareScript, encryptScript } from "./script.js";
17
21
  import { spawn } from "node:child_process";
18
22
  import { newMigration, runMigrations } from "./migrate.js";
19
23
  import {
20
24
  APP_CONFIG_NAME,
25
+ buildEnsureConfigTableSql,
26
+ buildPostgresConfigUpsertSql,
27
+ buildPostgresApplicationConfigLookupSql,
28
+ buildPostgresSetAppSql,
21
29
  extractConfigVersion,
22
30
  getChunkedUpsertBlocks,
23
31
  getUpsertBlock,
24
32
  isSemanticVersion,
25
33
  parseConfigForVersion,
34
+ readConfigRows,
26
35
  } from "../engine/domain/appConfig.js";
36
+ import {
37
+ isPostgresIndex,
38
+ execPostgres,
39
+ primeRegistry,
40
+ } from "../engine/domain/dialect.js";
41
+ import { addUser } from "../engine/orm/userAdmin.js";
42
+ import {
43
+ preservePublishedAdditions,
44
+ renderApplicationConfig,
45
+ uploadSourceConfigName,
46
+ } from "../engine/domain/publishedAdditions.js";
47
+ import { validateTenancyDeclaration } from "../engine/orm/tenancyDeclaration.js";
48
+ import { logger } from "../logger.js";
27
49
 
28
50
  const getKeyFolderPath = () => {
29
51
  const scriptPath =
@@ -68,6 +90,23 @@ const options = {
68
90
  type: "number",
69
91
  demandOption: false,
70
92
  },
93
+ /*
94
+ * ⚠️ NOT the same link as `-s/--server`. `-s` is the FINA/hub URL the bundle is POSTed to;
95
+ * this picks the Biz-A server that issues the bundle-encryption key, which `prepareKeys` must
96
+ * fetch BEFORE anything is uploaded. It was hard-coded to production, so `biza add` could not
97
+ * run against a local stack at all — it died on `ENOTFOUND server.biz-a.id` while every other
98
+ * local service was healthy. `choices` + the `.strict()` below are what stop a typo from
99
+ * silently falling back to production; see `envs/serverEndpoints.js`.
100
+ */
101
+ serverMode: {
102
+ alias: "sMode",
103
+ describe:
104
+ "Which Biz-A backend issues the bundle encryption key: prod, dev, or local",
105
+ type: "string",
106
+ choices: SERVER_MODES,
107
+ default: "prod",
108
+ demandOption: false,
109
+ },
71
110
  };
72
111
 
73
112
  const addCommandOptions = {
@@ -85,6 +124,13 @@ const addCommandOptions = {
85
124
  demandOption: false,
86
125
  default: false,
87
126
  },
127
+ include: {
128
+ describe:
129
+ "Config mode only: additional directories whose DOMAIN configs upload alongside this one's — for a Layer 3 domain package that lives in its own tree (repeatable). An application.js in an included directory is refused, never uploaded: that row belongs to -d.",
130
+ type: "array",
131
+ demandOption: false,
132
+ default: [],
133
+ },
88
134
  publish: {
89
135
  describe:
90
136
  "Config mode only (directory contains application.js): after saving, publish the domain DDL then the application config — the same as the admin App Config Publish button",
@@ -111,7 +157,7 @@ const removeCommandOptions = {
111
157
  },
112
158
  };
113
159
 
114
- const prepareKeys = async () => {
160
+ const prepareKeys = async (serverLink = env.BIZA_SERVER_LINK) => {
115
161
  const data = Buffer.from(
116
162
  JSON.stringify({ issuer: "CLI", acquirer: "Client" }),
117
163
  ).toString("base64");
@@ -121,7 +167,7 @@ const prepareKeys = async () => {
121
167
  passphrase: "Biz-A@cli",
122
168
  padding: cryptoConstants.RSA_PKCS1_PSS_PADDING,
123
169
  }).toString("base64");
124
- const res = await axios.get(env.BIZA_SERVER_LINK + "/api/issuerKey", {
170
+ const res = await axios.get(serverLink + "/api/issuerKey", {
125
171
  params: { data, signature },
126
172
  });
127
173
  if (
@@ -408,8 +454,64 @@ const resolveUpsertBlocks = (name, version, content, dbIndex) => {
408
454
  WHOLE set before anything is uploaded, so a stray lib or a typo'd metadata can never leave a
409
455
  half-applied config in SYS$CONFIG. Every problem is collected and reported at once, each
410
456
  naming its file. */
411
- const collectConfigUploads = ({ workingDir, fileNames = [] }) => {
457
+ /*
458
+ * ⚠️ ADDITIONAL CONFIG DIRECTORIES (`--include`), for a Layer 3 domain package that lives in its
459
+ * own tree rather than beside the host's `application.js`.
460
+ *
461
+ * Config mode triggers on an `application.js` in the ONE directory `-d` names, so before this a
462
+ * package like `/biz-a-template/hr/src/config/` had no upload path whatsoever — and registering it in
463
+ * the host's `domains` map was INERT rather than broken, because the runtime loader skips a named
464
+ * domain with no row (`if (!row?.data) continue`). A domain that silently is not there is worse than
465
+ * one that fails loudly.
466
+ *
467
+ * ⚠️ AN `application.js` IN AN INCLUDED DIRECTORY IS REFUSED, NOT SKIPPED. Uploading it would
468
+ * replace the HOST's APPLICATION_CONFIG — its navigation, its domains map — with a package's, and the
469
+ * only symptom would be a console that lost its menus. Pointing `--include` at a host directory is a
470
+ * mistake worth being told about.
471
+ */
472
+ const collectIncludedConfigFiles = (include = []) => {
412
473
  const problems = [];
474
+ const files = [];
475
+
476
+ for (const entry of Array.isArray(include) ? include : [include]) {
477
+ const dir = String(entry ?? "").trim();
478
+ if (!dir) continue;
479
+ const resolved = path.resolve(dir);
480
+
481
+ let names;
482
+ try {
483
+ if (!fs.statSync(resolved).isDirectory()) {
484
+ problems.push(`--include ${dir}: not a directory.`);
485
+ continue;
486
+ }
487
+ names = fs.readdirSync(resolved);
488
+ } catch (error) {
489
+ problems.push(`--include ${dir}: ${error?.message ?? error}`);
490
+ continue;
491
+ }
492
+
493
+ const configs = names.filter((name) => name.toLowerCase().endsWith(".js"));
494
+ if (configs.some((name) => name.toLowerCase() === APPLICATION_CONFIG_FILE)) {
495
+ problems.push(
496
+ `--include ${dir}: contains ${APPLICATION_CONFIG_FILE}. An included directory supplies ` +
497
+ "DOMAIN configs only — the application config belongs to the directory named by -d.",
498
+ );
499
+ continue;
500
+ }
501
+ for (const name of configs) files.push(path.join(resolved, name));
502
+ }
503
+
504
+ return { files, problems };
505
+ };
506
+
507
+ /* PURE-ish, continued: `include` names extra directories whose DOMAIN configs join the set. They are
508
+ folded in BEFORE validation so an included file gets exactly the same checks — and the same
509
+ "nothing was uploaded" guarantee — as one sitting beside application.js. */
510
+ const collectConfigUploads = ({ workingDir, fileNames = [], include = [] }) => {
511
+ const problems = [];
512
+ const included = collectIncludedConfigFiles(include);
513
+ problems.push(...included.problems);
514
+ fileNames = [...fileNames, ...included.files];
413
515
  const readConfig = (fileName) =>
414
516
  fs
415
517
  .readFileSync(
@@ -444,6 +546,10 @@ const collectConfigUploads = ({ workingDir, fileNames = [] }) => {
444
546
  }
445
547
 
446
548
  const domains = [];
549
+ /* ⚠️ Kept because the tenancy rule is CROSS-FILE: a scoped entity in one domain may be given its
550
+ foreign key by a relationship in another, and `buildTenancyMap` folds them all together at
551
+ runtime. Validating file-by-file would refuse a declaration the runtime accepts. */
552
+ const parsedConfigs = [];
447
553
  const seenNames = new Map();
448
554
  for (const fileName of fileNames) {
449
555
  const baseName = path.basename(String(fileName));
@@ -485,9 +591,16 @@ const collectConfigUploads = ({ workingDir, fileNames = [] }) => {
485
591
  continue;
486
592
  }
487
593
  seenNames.set(name, baseName);
594
+ parsedConfigs.push(parsed);
488
595
  domains.push({ name, version, content, fileName: baseName });
489
596
  }
490
597
 
598
+ /* Doc 3 §11 — refuse a tenancy declaration the schema does not actually back. This is the last
599
+ point where every config is in hand and NOTHING has been written to sys$config, which is what
600
+ makes "nothing was uploaded" true. See engine/orm/tenancyDeclaration.js for why it refuses
601
+ rather than warns. */
602
+ problems.push(...validateTenancyDeclaration(parsedConfigs));
603
+
491
604
  if (problems.length > 0) {
492
605
  throw new Error(
493
606
  `Config upload aborted, nothing was uploaded:\n - ${problems.join("\n - ")}`,
@@ -501,19 +614,86 @@ const collectConfigUploads = ({ workingDir, fileNames = [] }) => {
501
614
  matches appConfig.service.ts publishEditorConfigs: every domain DDL publish first (sequential),
502
615
  then the application-config publish (which regenerates template scaffolding + rebuilds the
503
616
  bundle). */
617
+ /*
618
+ * Fold the publisher's own additions back into the file about to be uploaded.
619
+ *
620
+ * Reads through `readConfigRows`, which is already dialect-aware, so this behaves identically on a
621
+ * Firebird tenant without a line of new SQL. Fails SOFT: if either row cannot be read we upload the
622
+ * file exactly as before, because preserving on a guess is what caused the earlier incident.
623
+ */
624
+ const preserveApplicationAdditions = async (application, apiConfig) => {
625
+ const readParsed = async (name) => {
626
+ try {
627
+ const rows = await readConfigRows(name, application.version, apiConfig);
628
+ const data = rows?.[0]?.data;
629
+ return data ? parseConfigForVersion(String(data)) : null;
630
+ } catch {
631
+ return null;
632
+ }
633
+ };
634
+
635
+ let fileConfig;
636
+ try {
637
+ fileConfig = parseConfigForVersion(application.content);
638
+ } catch {
639
+ return { application };
640
+ }
641
+
642
+ const [storedConfig, lastUploadedConfig] = await Promise.all([
643
+ readParsed(application.name),
644
+ readParsed(uploadSourceConfigName(application.name)),
645
+ ]);
646
+
647
+ const kept = preservePublishedAdditions({ fileConfig, storedConfig, lastUploadedConfig });
648
+ const keptKeys = Object.keys(kept);
649
+ if (keptKeys.length === 0) {
650
+ /* Say WHY nothing was kept when there plainly was something to keep — a silent revert is how
651
+ this cost four round-trips before anyone worked out what was happening. */
652
+ if (storedConfig && !lastUploadedConfig) {
653
+ logger.info(
654
+ `[${application.name}] no record of a previous upload, so nothing is preserved; ` +
655
+ "re-run the application's own publish afterwards to regenerate what it adds.",
656
+ );
657
+ }
658
+ return { application };
659
+ }
660
+
661
+ logger.info(
662
+ `[${application.name}] keeping ${keptKeys.join(", ")} from the last application publish ` +
663
+ "(unchanged in application.js); every other key comes from the file.",
664
+ );
665
+ return {
666
+ application: {
667
+ ...application,
668
+ content: renderApplicationConfig({ ...fileConfig, ...kept }),
669
+ },
670
+ };
671
+ };
672
+
504
673
  async function uploadConfigs({
505
674
  workingDir,
506
675
  fileNames,
676
+ include = [],
507
677
  server,
508
678
  apiPort,
509
679
  dbIndex,
510
680
  sub,
511
681
  publish = false,
512
682
  forceFull = false,
683
+ serverMode = "prod",
513
684
  }) {
685
+ /* Phase B2: ask biz-a for this tenant's database coordinates before anything reads the
686
+ dialect. A deployed CLI has no config/databases.json, so without this the registry
687
+ indexes would look like Firebird. No-op when --sub is absent or biz-a is unreachable. */
688
+ await primeRegistry({
689
+ subdomain: sub,
690
+ serverLink: resolveServerLink(serverMode),
691
+ });
692
+
514
693
  const { application, domains } = collectConfigUploads({
515
694
  workingDir,
516
695
  fileNames,
696
+ include,
517
697
  });
518
698
 
519
699
  const apiConfig = {
@@ -523,17 +703,51 @@ async function uploadConfigs({
523
703
  };
524
704
 
525
705
  const saveOne = async ({ name, version, content }) => {
706
+ /* A PostgreSQL index writes straight to its pool. No chunking: the block splitting below
707
+ exists only because the FINA HTTP body has a size limit, which a direct connection does
708
+ not. */
709
+ if (isPostgresIndex(dbIndex)) {
710
+ await execPostgres(buildEnsureConfigTableSql(), dbIndex);
711
+ await execPostgres(
712
+ buildPostgresConfigUpsertSql(name, version, content, false),
713
+ dbIndex,
714
+ );
715
+ logger.info(`saved ${name}@${version} (postgres db${dbIndex})`);
716
+ return;
717
+ }
718
+
526
719
  const blocks = resolveUpsertBlocks(name, version, content, dbIndex);
527
720
  for (const block of blocks) {
528
721
  await executeConfigBlock(block, { server, apiPort, dbIndex });
529
722
  }
530
- console.log(
723
+ logger.info(
531
724
  `saved ${name}@${version}${blocks.length > 1 ? ` (${blocks.length} chunks)` : ""}`,
532
725
  );
533
726
  };
534
727
 
535
728
  if (application) {
536
- await saveOne(application);
729
+ /*
730
+ * ⚠️ APPLICATION_CONFIG HAS TWO WRITERS. This one uploads `application.js` verbatim — which
731
+ * deliberately ships only the BOOTSTRAP navigation and the hand-authored domains — while the
732
+ * APPLICATION's own publish rebuilds the same row with what it GENERATES (injected staff
733
+ * consoles, the generated runtime domain). Overwriting it wholesale wiped every app menu until
734
+ * someone re-ran the app publish by hand.
735
+ *
736
+ * ⚠️ The obvious fix is the one that already failed: "prefer whatever is stored" dropped the
737
+ * generated domain and made the damage permanent. The publisher owns SOME keys and the author
738
+ * owns the rest, so the question is not who wins but whether the AUTHOR touched this key —
739
+ * answerable only against the previously uploaded file. See engine/domain/publishedAdditions.js.
740
+ */
741
+ const preserved = await preserveApplicationAdditions(application, apiConfig);
742
+ await saveOne(preserved.application);
743
+ /* Record what was uploaded, so the NEXT run can tell an author edit from an untouched key.
744
+ Written after the config itself: a failed upload must not leave a source row claiming the
745
+ author's file is already stored. */
746
+ await saveOne({
747
+ name: uploadSourceConfigName(application.name),
748
+ version: application.version,
749
+ content: application.content,
750
+ });
537
751
  }
538
752
  for (const domain of domains) {
539
753
  await saveOne(domain);
@@ -561,6 +775,10 @@ async function uploadConfigs({
561
775
  port: apiPort,
562
776
  dbindex: dbIndex,
563
777
  subdomain: sub,
778
+ /* publishApplicationConfig -> publishApp -> addApp re-enters bundle mode and fetches the
779
+ issuer key again, so the mode has to survive the round trip or the publish half of
780
+ `--publish` still dials production. */
781
+ serverMode,
564
782
  };
565
783
 
566
784
  for (const domain of domains) {
@@ -579,7 +797,7 @@ async function uploadConfigs({
579
797
  `Publish ${domain.name}@${domain.version} failed: ${result?.error ?? "no error message from server"}`,
580
798
  );
581
799
  }
582
- console.log(`published ${domain.name}@${domain.version}`);
800
+ logger.info(`published ${domain.name}@${domain.version}`);
583
801
  }
584
802
 
585
803
  if (application) {
@@ -608,7 +826,7 @@ async function uploadConfigs({
608
826
  `Publish ${application.name}@${application.version} failed: ${result?.error ?? "no error message from server"}`,
609
827
  );
610
828
  }
611
- console.log(`published ${application.name}@${application.version}`);
829
+ logger.info(`published ${application.name}@${application.version}`);
612
830
  } finally {
613
831
  process.chdir(previousCwd);
614
832
  fs.rmSync(scratchDir, { force: true, recursive: true });
@@ -657,6 +875,22 @@ const resolveAppDisplayNameFromApplicationConfig = async ({
657
875
  dbIndex,
658
876
  sub,
659
877
  }) => {
878
+ /* A PostgreSQL tenant reads SYS$CONFIG directly. This must not silently return "": the
879
+ display name keys the SYS$APPS row, so falling back to the random bundle name would insert
880
+ a fresh app row on every publish instead of updating the existing one. */
881
+ if (isPostgresIndex(dbIndex)) {
882
+ try {
883
+ const rows = await execPostgres(
884
+ buildPostgresApplicationConfigLookupSql(),
885
+ dbIndex,
886
+ );
887
+ const displayName = parseApplicationConfigData(rows?.[0]?.data)?.metadata?.name;
888
+ return typeof displayName === "string" ? displayName.trim() : "";
889
+ } catch {
890
+ return "";
891
+ }
892
+ }
893
+
660
894
  if (typeof axios.request !== "function") {
661
895
  return "";
662
896
  }
@@ -728,6 +962,9 @@ async function addApp({
728
962
  body = null,
729
963
  publish = false,
730
964
  forceFull = false,
965
+ serverMode = "prod",
966
+ /* Extra directories whose DOMAIN configs upload alongside this one's (see --include). */
967
+ include = [],
731
968
  } = {}) {
732
969
  const oldCwd = process.cwd();
733
970
 
@@ -774,20 +1011,22 @@ async function addApp({
774
1011
  return await uploadConfigs({
775
1012
  workingDir: process.cwd(),
776
1013
  fileNames: mergedSourceFiles,
1014
+ include,
777
1015
  server,
778
1016
  apiPort,
779
1017
  dbIndex,
780
1018
  sub,
781
1019
  publish,
782
1020
  forceFull,
1021
+ serverMode,
783
1022
  });
784
1023
  }
785
1024
 
786
1025
  if (mergedSourceFiles.length > 0) {
787
- const keys = await prepareKeys();
1026
+ const keys = await prepareKeys(resolveServerLink(serverMode));
788
1027
  if (!keys) {
789
1028
  const msg = "Can not prepare encryption keys";
790
- console.error(msg);
1029
+ logger.error(msg);
791
1030
  return { success: false, error: msg };
792
1031
  }
793
1032
 
@@ -825,7 +1064,7 @@ async function addApp({
825
1064
  }
826
1065
  const bundleFile = `${rootFolder}${bundleName}.tgz`;
827
1066
  compressIt(bundleFile, bundleFolder);
828
- console.log(
1067
+ logger.info(
829
1068
  `Finished packing ${processedFile} files into "${bundleFile}" (${((performance.now() - bundlingStart) / 1000).toFixed(2)}s)`,
830
1069
  );
831
1070
 
@@ -853,29 +1092,44 @@ async function addApp({
853
1092
  const uploadAppName = normalizeAppName(
854
1093
  (useApplicationConfigName ? appDisplayName : "") || bundleName,
855
1094
  );
1095
+ /* SYS$APPS is a Biz-A table, not a FINA one, so a PostgreSQL tenant is written
1096
+ directly — no FINA server in the path at all. On Firebird the FINA stored
1097
+ procedure TOrmMethod/"setApp" still owns the write. */
1098
+ if (isPostgresIndex(dbIndex)) {
1099
+ await execPostgres(
1100
+ buildPostgresSetAppSql(uploadAppName, data, keys.metadata),
1101
+ dbIndex,
1102
+ );
1103
+ logger.info(
1104
+ `Finished uploading "${bundleFile}" to postgres db${dbIndex} (${((performance.now() - uploadingStart) / 1000).toFixed(2)}s)`,
1105
+ );
1106
+ fs.rmSync(rootFolder, { force: true, recursive: true });
1107
+ return { success: true, data: { appName: uploadAppName } };
1108
+ }
1109
+
856
1110
  const param = {
857
1111
  _parameters: [dbIndex, uploadAppName, data, keys.metadata],
858
1112
  };
859
1113
 
860
1114
  const res = await axios.post(url, param, { headers: headers });
861
1115
  if (res.data.success) {
862
- console.log(
1116
+ logger.info(
863
1117
  `Finished uploading "${bundleFile}" (${((performance.now() - uploadingStart) / 1000).toFixed(2)}s)`,
864
1118
  );
865
1119
  fs.rmSync(rootFolder, { force: true, recursive: true });
866
1120
  return { success: true, data: res.data };
867
1121
  }
868
1122
 
869
- console.error(res.data.error);
1123
+ logger.error(res.data.error);
870
1124
  return { success: false, error: res.data.error, data: res.data };
871
1125
  }
872
1126
 
873
1127
  const msg = "Nothing to upload. Please recheck your app folder.";
874
- console.error(msg);
1128
+ logger.error(msg);
875
1129
  return { success: false, error: msg };
876
1130
  } catch (e) {
877
1131
  const errMsg = e.response?.data ? e.response.data : e;
878
- console.error(errMsg);
1132
+ logger.error(errMsg);
879
1133
  return { success: false, error: errMsg };
880
1134
  } finally {
881
1135
  process.chdir(oldCwd);
@@ -914,9 +1168,9 @@ async function runUnitTests(options) {
914
1168
  });
915
1169
 
916
1170
  child.on("close", async (code) => {
917
- console.log("====================");
918
- console.log(output);
919
- console.log("====================");
1171
+ logger.info("====================");
1172
+ logger.info(output);
1173
+ logger.info("====================");
920
1174
  const noTestsFound = output.includes("No tests found");
921
1175
  const missingJest =
922
1176
  output.includes("missing packages") && output.includes("jest");
@@ -937,9 +1191,10 @@ async function runUnitTests(options) {
937
1191
  files: options.files,
938
1192
  publish: options.publish,
939
1193
  forceFull: options.forceFull,
1194
+ serverMode: options.serverMode,
940
1195
  });
941
1196
  } else {
942
- console.error("Biz-A Add aborted");
1197
+ logger.error("Biz-A Add aborted");
943
1198
  }
944
1199
  resolve();
945
1200
  });
@@ -969,6 +1224,8 @@ const buildCli = () =>
969
1224
  files: commandOptions.files,
970
1225
  publish: commandOptions.publish,
971
1226
  forceFull: commandOptions.forceFull,
1227
+ serverMode: commandOptions.serverMode,
1228
+ include: commandOptions.include,
972
1229
  });
973
1230
  return;
974
1231
  }
@@ -999,7 +1256,7 @@ const buildCli = () =>
999
1256
  });
1000
1257
  if (res.data?.success) {
1001
1258
  if (deleteApps == "") {
1002
- console.log("All apps removed");
1259
+ logger.info("All apps removed");
1003
1260
  } else {
1004
1261
  const failedList =
1005
1262
  res.data._f &&
@@ -1012,18 +1269,18 @@ const buildCli = () =>
1012
1269
  : [];
1013
1270
  const removeList = deleteApps.split(",");
1014
1271
  removeList.forEach((app) => {
1015
- console.log(
1272
+ logger.info(
1016
1273
  `${app} ${failedList.indexOf(app) == -1 ? "removed" : "not found"}`,
1017
1274
  );
1018
1275
  });
1019
1276
  }
1020
1277
  } else {
1021
- console.error(res.data.error);
1278
+ logger.error(res.data.error);
1022
1279
  }
1023
1280
  return res;
1024
1281
  } catch (e) {
1025
1282
  const errMsg = e.response?.data ? e.response.data : e;
1026
- console.error(errMsg);
1283
+ logger.error(errMsg);
1027
1284
  return errMsg;
1028
1285
  }
1029
1286
  })();
@@ -1043,6 +1300,85 @@ const buildCli = () =>
1043
1300
  },
1044
1301
  (commandOptions) => newMigration(commandOptions),
1045
1302
  )
1303
+ .command(
1304
+ "user:add",
1305
+ "Create a platform user in SYS$USERS on a PostgreSQL tenant (defaults: supervisor/supervisor)",
1306
+ (yargs) =>
1307
+ yargs
1308
+ .option("u", {
1309
+ alias: "username",
1310
+ describe: "User name (stored upper-cased, as the login expects)",
1311
+ type: "string",
1312
+ default: "supervisor",
1313
+ })
1314
+ .option("w", {
1315
+ alias: "password",
1316
+ describe: "Password. Hashed with scrypt before it is stored",
1317
+ type: "string",
1318
+ default: "supervisor",
1319
+ })
1320
+ .option("fullname", {
1321
+ describe: "Display name (defaults to the user name)",
1322
+ type: "string",
1323
+ })
1324
+ .option("userlevel", {
1325
+ describe: "User level. Anything below 0 cannot log in",
1326
+ type: "number",
1327
+ default: 9,
1328
+ })
1329
+ .option("replace", {
1330
+ describe:
1331
+ "Reset the password if the user already exists. Without it an existing user is left untouched",
1332
+ type: "boolean",
1333
+ default: false,
1334
+ })
1335
+ .option("admin", {
1336
+ describe:
1337
+ "Grant admin (SYS$ADMINISTRATOR). The FIRST user is granted automatically - without one, nobody can reach the admin screens or publish",
1338
+ type: "boolean",
1339
+ default: false,
1340
+ }),
1341
+ async (commandOptions) => {
1342
+ const dbIndex = Number(commandOptions.dbIndex);
1343
+ try {
1344
+ await primeRegistry({
1345
+ subdomain: commandOptions.sub,
1346
+ serverLink: resolveServerLink(
1347
+ commandOptions.serverMode,
1348
+ ),
1349
+ });
1350
+ const result = await addUser(
1351
+ {
1352
+ username: commandOptions.username,
1353
+ password: commandOptions.password,
1354
+ fullname: commandOptions.fullname,
1355
+ userlevel: commandOptions.userlevel,
1356
+ replace: commandOptions.replace,
1357
+ admin: commandOptions.admin,
1358
+ },
1359
+ {
1360
+ exec: execPostgres,
1361
+ dbIndex,
1362
+ isPostgres: isPostgresIndex(dbIndex),
1363
+ },
1364
+ );
1365
+ if (result.status === "exists") {
1366
+ logger.info(
1367
+ `user ${result.username} already exists in db${dbIndex} - password left untouched. Use --replace to reset it.` +
1368
+ (result.admin ? " Granted admin." : ""),
1369
+ );
1370
+ return;
1371
+ }
1372
+ logger.info(
1373
+ `user ${result.username} ${result.status} in db${dbIndex}` +
1374
+ (result.admin ? " and granted admin" : ""),
1375
+ );
1376
+ } catch (error) {
1377
+ logger.error(`user:add failed: ${error?.message ?? error}`);
1378
+ process.exitCode = 1;
1379
+ }
1380
+ },
1381
+ )
1046
1382
  .command(
1047
1383
  "migrate:run",
1048
1384
  "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