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.
- package/bin/app.js +358 -22
- package/bin/deleteApp.js +4 -3
- package/bin/directHubEvent.js +38 -28
- package/bin/hub.js +501 -352
- package/bin/hubEvent.js +833 -505
- package/bin/index.js +5 -4
- package/bin/migrate.js +292 -173
- package/bin/proxy.js +26 -25
- package/bin/script.js +125 -124
- package/bin/tunnelTarget.js +41 -0
- package/bin/uploadApp.js +6 -5
- package/bin/watcher.js +49 -63
- package/callbackController.js +65 -64
- package/config/databases.example.json +42 -0
- package/config/databases.json +20 -0
- package/config/ext-oidc.json +7 -0
- package/config/secrets.json +21 -0
- package/db/apiUrl.js +58 -0
- package/db/db.js +396 -43
- package/engine/bpm/bpm-agent.js +467 -385
- package/engine/bpm/routes.js +66 -61
- package/engine/bpm/workflow-runtime.js +1676 -1626
- package/engine/bpm/workflow.js +290 -283
- package/engine/domain/appConfig.js +521 -92
- package/engine/domain/applicationConfig.js +7 -7
- package/engine/domain/dialect.js +323 -0
- package/engine/domain/entityConfigLoader.js +100 -0
- package/engine/domain/entityConfigPrimer.js +97 -0
- package/engine/domain/firebird-ddl.js +41 -14
- package/engine/domain/naming.js +47 -0
- package/engine/domain/postgres-ddl.js +625 -0
- package/engine/domain/publishedAdditions.js +69 -0
- package/engine/domain/system.js +3 -0
- package/engine/ext/channel.js +3 -0
- package/engine/ext/dispatch.js +19 -2
- package/engine/ext/live.js +19 -0
- package/engine/ext/oidc.js +1 -1
- package/engine/ext/routes.js +2 -1
- package/engine/ext/token.js +9 -11
- package/engine/orm/apiRoute.js +792 -0
- package/engine/orm/changeInference.js +261 -0
- package/engine/orm/concurrency.js +132 -0
- package/engine/orm/connection.js +294 -0
- package/engine/orm/crudQuery.js +288 -0
- package/engine/orm/dataSnapShape.js +127 -0
- package/engine/orm/dialect.js +41 -0
- package/engine/orm/entityConfig.js +643 -0
- package/engine/orm/executeBlock.js +728 -0
- package/engine/orm/executor.js +291 -0
- package/engine/orm/finaAuth.js +0 -0
- package/engine/orm/finaEndpoint.js +230 -0
- package/engine/orm/imgQuery.js +82 -0
- package/engine/orm/index.js +255 -0
- package/engine/orm/joinChain.js +143 -0
- package/engine/orm/listQuery.js +224 -0
- package/engine/orm/masterDetail.js +528 -0
- package/engine/orm/registry.js +92 -0
- package/engine/orm/registryClient.js +63 -0
- package/engine/orm/requestScope.js +157 -0
- package/engine/orm/rlsApplier.js +74 -0
- package/engine/orm/rlsPolicy.js +370 -0
- package/engine/orm/sessionStore.js +337 -0
- package/engine/orm/tenancyDeclaration.js +268 -0
- package/engine/orm/tenantApi.js +416 -0
- package/engine/orm/tenantContext.js +216 -0
- package/engine/orm/tenantResolution.js +426 -0
- package/engine/orm/tenantScope.js +82 -0
- package/engine/orm/userAdmin.js +98 -0
- package/envs/env.dev.js +7 -2
- package/envs/env.js +7 -2
- package/envs/hosts.js +36 -0
- package/envs/serverEndpoints.js +50 -0
- package/logger.js +122 -0
- package/mailController.js +19 -18
- package/message-broker/agent.js +63 -62
- package/message-broker/clientEmitter.js +304 -310
- package/message-broker/dispatcher.js +161 -158
- package/message-broker/orchestrator.js +2 -2
- package/migrations/1788527682689__privileges.sql +103 -0
- package/migrations/postgres/1777727800000__config.sql +21 -0
- package/migrations/postgres/1777727810000__sys_runtime.sql +124 -0
- package/migrations/postgres/1777727873750__apps.sql +30 -0
- package/migrations/postgres/1777727892577__bpm.sql +242 -0
- package/migrations/postgres/1787137466726__ext.sql +74 -0
- package/migrations/postgres/1787201361442__ext_cli.sql +12 -0
- package/migrations/postgres/1788527682689__privileges.sql +68 -0
- package/migrations/postgres/1789040354328__tenancy.sql +33 -0
- package/migrations/postgres/1789200000000__rls_role.sql +46 -0
- package/migrations/postgres/1789350000000__user_tenant.sql +111 -0
- package/migrations/postgres/1789500000000__session.sql +73 -0
- package/package.json +6 -1
- package/proxyController.js +83 -82
- package/readme.md +5 -5
- package/scheduler/configController.js +116 -115
- package/scheduler/datalib.js +355 -364
- package/scheduler/timer.js +88 -87
- package/scheduler/watcherController.js +94 -93
- package/security/internalPrincipal.js +201 -0
- package/security/secretProvider.js +6 -1
- package/security/tokenFormat.js +38 -0
- package/worker/cliScriptWorker.js +234 -186
- 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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1272
|
+
logger.info(
|
|
1016
1273
|
`${app} ${failedList.indexOf(app) == -1 ? "removed" : "not found"}`,
|
|
1017
1274
|
);
|
|
1018
1275
|
});
|
|
1019
1276
|
}
|
|
1020
1277
|
} else {
|
|
1021
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
+
logger.info(`${deleteApps=='' ? 'All' : deleteApps} apps removed`)
|
|
56
57
|
} else {
|
|
57
|
-
|
|
58
|
+
logger.error(res.data.error)
|
|
58
59
|
}
|
|
59
60
|
} catch (e) {
|
|
60
|
-
|
|
61
|
+
logger.error(e.response?.data ? e.response.data : e)
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
|