biz-a-cli 2.3.80-15419 → 2.3.80
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 +20 -255
- package/bin/hub.js +0 -16
- package/bin/hubEvent.js +5 -98
- package/bin/tunnelTarget.js +0 -4
- package/engine/bpm/workflow.js +6 -7
- package/engine/cc/configValidate.js +0 -53
- package/engine/cc/guides/V2-SCREEN-AUTHORING.md +512 -531
- package/engine/cc/layer3Behavior.js +1 -2
- package/engine/cc/layer3Schema.js +1 -3
- package/engine/cc/livePipelines.js +3 -9
- package/engine/cc/publish.js +105 -86
- package/engine/domain/appConfig.js +18 -108
- package/engine/domain/applicationConfig.js +12 -87
- package/engine/domain/dialect.js +0 -138
- package/engine/domain/domainNavigation.js +50 -86
- package/engine/domain/entityConfigLoader.js +4 -16
- package/engine/domain/numericScale.js +0 -2
- package/engine/domain/system.js +0 -2
- package/engine/ext/publicTheme.js +1 -8
- package/engine/orm/apiRoute.js +61 -187
- package/engine/orm/entityConfig.js +2 -32
- package/engine/orm/finaAuth.js +0 -0
- package/engine/orm/finaEndpoint.js +0 -19
- package/engine/orm/imgQuery.js +0 -92
- package/engine/orm/listQuery.js +1 -6
- package/package.json +1 -1
- package/worker/cliScriptWorker.js +0 -5
- package/engine/attachments/store.js +0 -195
- package/engine/domain/domainRegistration.js +0 -65
- package/engine/fina/approval.js +0 -141
- package/engine/fina/approvalService.js +0 -59
- package/engine/fina/attachments.js +0 -165
- package/engine/fina/client.js +0 -84
- package/engine/fina/config.js +0 -26
- package/engine/fina/context.js +0 -58
- package/engine/fina/document.js +0 -67
- package/engine/fina/documentWrite.js +0 -173
- package/engine/fina/documents/index.js +0 -28
- package/engine/fina/documents/jv.js +0 -105
- package/engine/fina/documents/jvWrite.js +0 -260
- package/engine/fina/documents/si.js +0 -40
- package/engine/fina/errors.js +0 -17
- package/engine/fina/features.js +0 -86
- package/engine/fina/guard.js +0 -102
- package/engine/fina/identity.js +0 -87
- package/engine/fina/live.js +0 -19
- package/engine/fina/lock.js +0 -35
- package/engine/fina/master.js +0 -331
- package/engine/fina/masters/coa.js +0 -104
- package/engine/fina/masters/currency.js +0 -39
- package/engine/fina/masters/department.js +0 -32
- package/engine/fina/masters/index.js +0 -21
- package/engine/fina/masters/project.js +0 -53
- package/engine/fina/period.js +0 -56
- package/engine/fina/print.js +0 -136
- package/engine/fina/readPolicy.js +0 -67
- package/engine/fina/recurring.js +0 -200
- package/engine/fina/route.js +0 -61
- package/engine/fina/routes/attachment.js +0 -41
- package/engine/fina/routes/document.js +0 -66
- package/engine/fina/routes/master.js +0 -26
- package/engine/fina/routes/period.js +0 -24
- package/engine/fina/routes/print.js +0 -19
- package/engine/fina/routes/recurring.js +0 -27
- package/engine/fina/routes/session.js +0 -23
- package/engine/fina/routes/setup.js +0 -58
- package/engine/fina/routes/submission.js +0 -46
- package/engine/fina/routes/template.js +0 -31
- package/engine/fina/settings.js +0 -159
- package/engine/fina/sql.js +0 -34
- package/engine/fina/submission.js +0 -290
- package/engine/fina/submissionCompare.js +0 -171
- package/engine/fina/submissionRepo.js +0 -114
- package/engine/fina/template.js +0 -118
- package/engine/fina/userMap.js +0 -79
- package/engine/orm/companyLocale.js +0 -74
- package/engine/orm/userApiDataset.js +0 -153
- package/migrations/postgres/1790600000000__fina.sql +0 -33
- package/migrations/postgres/1790700000000__attachment.sql +0 -29
- package/migrations/postgres/1790800000000__fina_jv_submission.sql +0 -41
package/bin/app.js
CHANGED
|
@@ -27,8 +27,6 @@ import {
|
|
|
27
27
|
} from "../engine/domain/themeConfig.js";
|
|
28
28
|
import {
|
|
29
29
|
APP_CONFIG_NAME,
|
|
30
|
-
appConfigNameFor,
|
|
31
|
-
secondaryAppNameOf,
|
|
32
30
|
buildEnsureConfigTableSql,
|
|
33
31
|
buildPostgresConfigUpsertSql,
|
|
34
32
|
buildPostgresApplicationConfigLookupSql,
|
|
@@ -58,7 +56,6 @@ import { validateTenancyDeclaration } from "../engine/orm/tenancyDeclaration.js"
|
|
|
58
56
|
import { logger } from "../logger.js";
|
|
59
57
|
import { findRawColorsInSource } from "../engine/domain/styleColors.js";
|
|
60
58
|
import { resolveMenuLayout } from "../engine/domain/domainNavigation.js";
|
|
61
|
-
import { bumpPatch, registerDomainVersions, unregisterDomains } from "../engine/domain/domainRegistration.js";
|
|
62
59
|
|
|
63
60
|
const getKeyFolderPath = () => {
|
|
64
61
|
const scriptPath =
|
|
@@ -165,13 +162,6 @@ const addCommandOptions = {
|
|
|
165
162
|
demandOption: false,
|
|
166
163
|
default: false,
|
|
167
164
|
},
|
|
168
|
-
register: {
|
|
169
|
-
describe:
|
|
170
|
-
"Config mode for a directory of DOMAIN configs only (no application.js): upload them and ADD them to the tenant's application config, keeping everything else in it (other apps, Control Center's domains, menus). For a second application on a tenant. PostgreSQL only",
|
|
171
|
-
type: "boolean",
|
|
172
|
-
demandOption: false,
|
|
173
|
-
default: false,
|
|
174
|
-
},
|
|
175
165
|
};
|
|
176
166
|
|
|
177
167
|
const removeCommandOptions = {
|
|
@@ -405,9 +395,7 @@ const findApplicationConfigFile = (fileNames = []) =>
|
|
|
405
395
|
gate exists to validate app scripts before they are bundled, and a config directory has nothing
|
|
406
396
|
to test — worse, jest exits non-zero there ("Could not find a config file", after traversing up),
|
|
407
397
|
which would abort the upload outright. */
|
|
408
|
-
const isConfigModeDir = (workingDir, files = null
|
|
409
|
-
/* Doc 8 — `--register` ships domain configs with no application.js: config mode by declaration. */
|
|
410
|
-
if (register) return true;
|
|
398
|
+
const isConfigModeDir = (workingDir, files = null) => {
|
|
411
399
|
try {
|
|
412
400
|
return !!findApplicationConfigFile(
|
|
413
401
|
getFileList({ workingDir: path.resolve(workingDir), files }),
|
|
@@ -537,15 +525,7 @@ const collectConfigUploads = ({ workingDir, fileNames = [], include = [] }) => {
|
|
|
537
525
|
`${path.basename(applicationFile)}: metadata.version "${version}" must be semantic (x.y.z).`,
|
|
538
526
|
);
|
|
539
527
|
}
|
|
540
|
-
|
|
541
|
-
as `APPLICATION_CONFIG.<app>` beside the host's (engine/domain/appConfig.js appConfigNameFor). */
|
|
542
|
-
let name = APP_CONFIG_NAME;
|
|
543
|
-
try {
|
|
544
|
-
name = appConfigNameFor(parseConfigForVersion(content)?.metadata);
|
|
545
|
-
} catch (error) {
|
|
546
|
-
problems.push(`${path.basename(applicationFile)}: ${error?.message ?? error}`);
|
|
547
|
-
}
|
|
548
|
-
application = { name, version, content };
|
|
528
|
+
application = { name: APP_CONFIG_NAME, version, content };
|
|
549
529
|
}
|
|
550
530
|
|
|
551
531
|
/*
|
|
@@ -563,12 +543,7 @@ const collectConfigUploads = ({ workingDir, fileNames = [], include = [] }) => {
|
|
|
563
543
|
const themeFile = fileNames.find(
|
|
564
544
|
(f) => path.basename(String(f)).toLowerCase() === THEME_CONFIG_FILE,
|
|
565
545
|
);
|
|
566
|
-
if (themeFile
|
|
567
|
-
problems.push(
|
|
568
|
-
`${path.basename(String(themeFile))}: the theme is the tenant's, published with its host application — ` +
|
|
569
|
-
`not with ${application.name}.`,
|
|
570
|
-
);
|
|
571
|
-
} else if (themeFile) {
|
|
546
|
+
if (themeFile) {
|
|
572
547
|
const { theme: parsedTheme, problems: themeProblems } = validateThemeSource(
|
|
573
548
|
readConfig(themeFile),
|
|
574
549
|
);
|
|
@@ -713,89 +688,6 @@ const preserveApplicationAdditions = async (application, apiConfig) => {
|
|
|
713
688
|
};
|
|
714
689
|
};
|
|
715
690
|
|
|
716
|
-
/*
|
|
717
|
-
* Doc 8 — `--register`: what the domains are added to, read BEFORE anything is written.
|
|
718
|
-
*
|
|
719
|
-
* ⚠️ The tenant's NEWEST application config, the row every reader takes (buildPostgresApplicationConfigLookupSql).
|
|
720
|
-
* Refused when the directory carries its own application.js (that is the replacing mode), on Firebird (PostgreSQL
|
|
721
|
-
* only, as Control Center), and when the tenant has no application config yet (there is nothing to add to — the
|
|
722
|
-
* host application comes first).
|
|
723
|
-
*/
|
|
724
|
-
const readRegistrationBase = async ({ application, dbIndex }) => {
|
|
725
|
-
if (application) {
|
|
726
|
-
throw new Error(
|
|
727
|
-
"--register adds domains to the tenant's application config, but this directory has application.js, which would " +
|
|
728
|
-
"REPLACE it. Upload the domains without application.js, or drop --register.",
|
|
729
|
-
);
|
|
730
|
-
}
|
|
731
|
-
if (!isPostgresIndex(dbIndex)) {
|
|
732
|
-
throw new Error(`--register needs a PostgreSQL database; index ${dbIndex} is not one.`);
|
|
733
|
-
}
|
|
734
|
-
const row = (await execPostgres(buildPostgresApplicationConfigLookupSql(), dbIndex))?.[0];
|
|
735
|
-
if (!row?.data) {
|
|
736
|
-
throw new Error(
|
|
737
|
-
"--register: this database has no application config to add the domains to. Upload the host application first.",
|
|
738
|
-
);
|
|
739
|
-
}
|
|
740
|
-
return { version: String(row.version ?? ""), config: parseConfigForVersion(String(row.data)) };
|
|
741
|
-
};
|
|
742
|
-
|
|
743
|
-
/*
|
|
744
|
-
* Doc 8 — `--register`: name the uploaded domains in the application config, as Control Center's publish does
|
|
745
|
-
* (engine/domain/domainRegistration.js). Answers the row the application publish must rebuild.
|
|
746
|
-
*
|
|
747
|
-
* Already named at these versions → nothing is written. Otherwise a NEW patch version, so the row every reader takes
|
|
748
|
-
* is the one that names them; every other key (other apps' domains, menus, theme) comes through unchanged.
|
|
749
|
-
*/
|
|
750
|
-
const registerUploadedDomains = async ({ registration, domains, dbIndex, saveOne }) => {
|
|
751
|
-
const next = registerDomainVersions(registration.config, new Map(domains.map(({ name, version }) => [name, version])));
|
|
752
|
-
if (JSON.stringify(next) === JSON.stringify(registration.config)) {
|
|
753
|
-
logger.info(`${APP_CONFIG_NAME}@${registration.version} already names ${domains.map((d) => `${d.name}@${d.version}`).join(", ")}; not rewritten`);
|
|
754
|
-
return { name: APP_CONFIG_NAME, version: registration.version };
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
const version = bumpPatch(registration.version);
|
|
758
|
-
if (next.metadata && typeof next.metadata === "object" && next.metadata.version !== undefined) next.metadata.version = version;
|
|
759
|
-
await saveOne({ name: APP_CONFIG_NAME, version, content: renderApplicationConfig(next) });
|
|
760
|
-
logger.info(`registered ${domains.map((d) => d.name).join(", ")} in ${APP_CONFIG_NAME}@${version}`);
|
|
761
|
-
|
|
762
|
-
/* Control Center checks a draft against the row it started from, so an open application draft is now behind. */
|
|
763
|
-
try {
|
|
764
|
-
const open = await execPostgres("SELECT ID FROM SYS$CC_DRAFT WHERE KIND = 'APPLICATION' AND STATUS = 'OPEN'", dbIndex);
|
|
765
|
-
if (open?.length) {
|
|
766
|
-
logger.warn(
|
|
767
|
-
"Control Center has an open application draft, which started from the previous application config. " +
|
|
768
|
-
"Publishing it will be refused as stale until it is re-opened from the current one.",
|
|
769
|
-
);
|
|
770
|
-
}
|
|
771
|
-
} catch {
|
|
772
|
-
/* No Control Center tables on this tenant: nothing to warn about. */
|
|
773
|
-
}
|
|
774
|
-
return { name: APP_CONFIG_NAME, version };
|
|
775
|
-
};
|
|
776
|
-
|
|
777
|
-
/*
|
|
778
|
-
* Doc 8 (2026-10-10) — a SECONDARY application (`APPLICATION_CONFIG.<app>`), checked BEFORE anything is written.
|
|
779
|
-
*
|
|
780
|
-
* PostgreSQL only, as --register. And never the host's own SYS$APPS row: the host's row is keyed by its display name,
|
|
781
|
-
* so an app named like it would publish over the host's bundle.
|
|
782
|
-
*/
|
|
783
|
-
const assertSecondaryApplication = async ({ application, dbIndex }) => {
|
|
784
|
-
const app = secondaryAppNameOf(application.name);
|
|
785
|
-
if (!isPostgresIndex(dbIndex)) {
|
|
786
|
-
throw new Error(`${application.name}: a second application on a tenant needs a PostgreSQL database; index ${dbIndex} is not one.`);
|
|
787
|
-
}
|
|
788
|
-
const host = (await execPostgres(buildPostgresApplicationConfigLookupSql(), dbIndex))?.[0];
|
|
789
|
-
const hostConfig = host?.data ? parseConfigForVersion(String(host.data)) : null;
|
|
790
|
-
const hostApp = normalizeAppName(hostConfig?.metadata?.name);
|
|
791
|
-
if (hostApp && hostApp === app) {
|
|
792
|
-
throw new Error(
|
|
793
|
-
`metadata.app "${app}" is the host application's own row (${APP_CONFIG_NAME} "${hostConfig.metadata.name}"); ` +
|
|
794
|
-
"name the second application something else.",
|
|
795
|
-
);
|
|
796
|
-
}
|
|
797
|
-
};
|
|
798
|
-
|
|
799
691
|
async function uploadConfigs({
|
|
800
692
|
workingDir,
|
|
801
693
|
fileNames,
|
|
@@ -808,7 +700,6 @@ async function uploadConfigs({
|
|
|
808
700
|
forceFull = false,
|
|
809
701
|
releaseCc = false,
|
|
810
702
|
serverMode = "prod",
|
|
811
|
-
register = false,
|
|
812
703
|
}) {
|
|
813
704
|
/* Phase B2: ask biz-a for this tenant's database coordinates before anything reads the
|
|
814
705
|
dialect. A deployed CLI has no config/databases.json, so without this the registry
|
|
@@ -824,11 +715,6 @@ async function uploadConfigs({
|
|
|
824
715
|
include,
|
|
825
716
|
});
|
|
826
717
|
|
|
827
|
-
/* Doc 8 — `--register`: these domains are ADDED to the tenant's application config, never replace it. Every
|
|
828
|
-
refusal happens here, before anything is written. */
|
|
829
|
-
const registration = register ? await readRegistrationBase({ application, dbIndex }) : null;
|
|
830
|
-
if (secondaryAppNameOf(application?.name)) await assertSecondaryApplication({ application, dbIndex });
|
|
831
|
-
|
|
832
718
|
const apiConfig = {
|
|
833
719
|
url: `${server}:${apiPort}`,
|
|
834
720
|
dbindex: dbIndex,
|
|
@@ -905,20 +791,12 @@ async function uploadConfigs({
|
|
|
905
791
|
await saveOne(domain);
|
|
906
792
|
}
|
|
907
793
|
|
|
908
|
-
/* The application config the publish below rebuilds: the uploaded one, or — with --register — the tenant's
|
|
909
|
-
newest one, now naming these domains. */
|
|
910
|
-
let applicationTarget = application ? { name: application.name, version: application.version } : null;
|
|
911
|
-
if (registration) {
|
|
912
|
-
applicationTarget = await registerUploadedDomains({ registration, domains, dbIndex, saveOne });
|
|
913
|
-
}
|
|
914
|
-
|
|
915
794
|
if (!publish) {
|
|
916
795
|
return {
|
|
917
796
|
success: true,
|
|
918
797
|
data: {
|
|
919
798
|
applicationConfig: application,
|
|
920
799
|
domains: domains.map(({ name, version }) => ({ name, version })),
|
|
921
|
-
...(registration ? { registeredIn: applicationTarget } : {}),
|
|
922
800
|
},
|
|
923
801
|
};
|
|
924
802
|
}
|
|
@@ -960,7 +838,7 @@ async function uploadConfigs({
|
|
|
960
838
|
logger.info(`published ${domain.name}@${domain.version}`);
|
|
961
839
|
}
|
|
962
840
|
|
|
963
|
-
if (
|
|
841
|
+
if (application) {
|
|
964
842
|
/* publishApplicationConfig -> publishApp -> addApp({ workingDir: process.cwd() }), and
|
|
965
843
|
addApp folds getFileList(cwd) into the bundle. cwd is the CONFIG directory right now, so
|
|
966
844
|
without this the configs themselves would be packed as app scripts. Run the publish from
|
|
@@ -973,8 +851,8 @@ async function uploadConfigs({
|
|
|
973
851
|
process.chdir(scratchDir);
|
|
974
852
|
const result = await publishApplicationConfig(
|
|
975
853
|
{
|
|
976
|
-
applicationConfigName:
|
|
977
|
-
applicationConfigVersion:
|
|
854
|
+
applicationConfigName: application.name,
|
|
855
|
+
applicationConfigVersion: application.version,
|
|
978
856
|
dbIndex,
|
|
979
857
|
url: apiConfig.url,
|
|
980
858
|
subdomain: sub,
|
|
@@ -983,10 +861,10 @@ async function uploadConfigs({
|
|
|
983
861
|
);
|
|
984
862
|
if (!result || result.success !== true) {
|
|
985
863
|
throw new Error(
|
|
986
|
-
`Publish ${
|
|
864
|
+
`Publish ${application.name}@${application.version} failed: ${result?.error ?? "no error message from server"}`,
|
|
987
865
|
);
|
|
988
866
|
}
|
|
989
|
-
logger.info(`published ${
|
|
867
|
+
logger.info(`published ${application.name}@${application.version}`);
|
|
990
868
|
} finally {
|
|
991
869
|
process.chdir(previousCwd);
|
|
992
870
|
fs.rmSync(scratchDir, { force: true, recursive: true });
|
|
@@ -1003,79 +881,6 @@ async function uploadConfigs({
|
|
|
1003
881
|
};
|
|
1004
882
|
}
|
|
1005
883
|
|
|
1006
|
-
/*
|
|
1007
|
-
* Doc 8 (2026-10-10) — `biza unregister --domains a,b [--publish]`: the inverse of --register. Takes the domains OUT of
|
|
1008
|
-
* the host APPLICATION_CONFIG (their entries and the menu leaves their publishes stamped; engine/domain/
|
|
1009
|
-
* domainRegistration.js unregisterDomains) as a new patch version, and with --publish rebuilds the host from it.
|
|
1010
|
-
*
|
|
1011
|
-
* The domain configs themselves stay in sys$config, and their tables stay in the database — only the host stops
|
|
1012
|
-
* naming them. How a domain moves out of the host into its own application.
|
|
1013
|
-
*
|
|
1014
|
-
* PostgreSQL only, as --register. A domain Control Center owns is refused: it is CC's to publish.
|
|
1015
|
-
*/
|
|
1016
|
-
async function unregisterAppDomains({
|
|
1017
|
-
domains,
|
|
1018
|
-
server = "http://localhost",
|
|
1019
|
-
apiPort = 212,
|
|
1020
|
-
dbIndex = 2,
|
|
1021
|
-
sub,
|
|
1022
|
-
publish = false,
|
|
1023
|
-
serverMode = "prod",
|
|
1024
|
-
} = {}) {
|
|
1025
|
-
try {
|
|
1026
|
-
await primeRegistry({ subdomain: sub, serverLink: resolveServerLink(serverMode) });
|
|
1027
|
-
|
|
1028
|
-
const names = String(domains ?? "")
|
|
1029
|
-
.split(",")
|
|
1030
|
-
.map((name) => name.trim())
|
|
1031
|
-
.filter(Boolean);
|
|
1032
|
-
if (names.length === 0) throw new Error("unregister: name the domains to take out with --domains a,b.");
|
|
1033
|
-
if (!isPostgresIndex(dbIndex)) throw new Error(`unregister needs a PostgreSQL database; index ${dbIndex} is not one.`);
|
|
1034
|
-
|
|
1035
|
-
await guardCcOwnedDomains({ exec: (sql) => execPostgres(sql, dbIndex), domainNames: names, releaseCc: false });
|
|
1036
|
-
|
|
1037
|
-
const row = (await execPostgres(buildPostgresApplicationConfigLookupSql(), dbIndex))?.[0];
|
|
1038
|
-
if (!row?.data) throw new Error("unregister: this database has no application config.");
|
|
1039
|
-
const current = parseConfigForVersion(String(row.data));
|
|
1040
|
-
const { config: next, removed } = unregisterDomains(current, names);
|
|
1041
|
-
if (removed.length === 0) {
|
|
1042
|
-
logger.info(`${APP_CONFIG_NAME}@${row.version} names none of ${names.join(", ")}; not rewritten`);
|
|
1043
|
-
return { success: true, data: { removed } };
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
const version = bumpPatch(String(row.version ?? ""));
|
|
1047
|
-
if (next.metadata && typeof next.metadata === "object" && next.metadata.version !== undefined) next.metadata.version = version;
|
|
1048
|
-
await execPostgres(buildEnsureConfigTableSql(), dbIndex);
|
|
1049
|
-
await execPostgres(buildPostgresConfigUpsertSql(APP_CONFIG_NAME, version, renderApplicationConfig(next), false), dbIndex);
|
|
1050
|
-
logger.info(`unregistered ${removed.join(", ")} from ${APP_CONFIG_NAME}@${version}`);
|
|
1051
|
-
|
|
1052
|
-
if (publish) {
|
|
1053
|
-
const { publishApplicationConfig } = await import("../engine/domain/applicationConfig.js");
|
|
1054
|
-
/* From an empty scratch dir, as uploadConfigs: addApp folds the working dir's own files into the bundle. */
|
|
1055
|
-
const scratchDir = fs.mkdtempSync(path.join(os.tmpdir(), "biza-publish-"));
|
|
1056
|
-
const previousCwd = process.cwd();
|
|
1057
|
-
try {
|
|
1058
|
-
process.chdir(scratchDir);
|
|
1059
|
-
const result = await publishApplicationConfig(
|
|
1060
|
-
{ applicationConfigName: APP_CONFIG_NAME, applicationConfigVersion: version, dbIndex, url: `${server}:${apiPort}`, subdomain: sub },
|
|
1061
|
-
{ server, port: apiPort, dbindex: dbIndex, subdomain: sub, serverMode },
|
|
1062
|
-
);
|
|
1063
|
-
if (!result || result.success !== true) {
|
|
1064
|
-
throw new Error(`Publish ${APP_CONFIG_NAME}@${version} failed: ${result?.error ?? "no error message from server"}`);
|
|
1065
|
-
}
|
|
1066
|
-
logger.info(`published ${APP_CONFIG_NAME}@${version}`);
|
|
1067
|
-
} finally {
|
|
1068
|
-
process.chdir(previousCwd);
|
|
1069
|
-
fs.rmSync(scratchDir, { force: true, recursive: true });
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
return { success: true, data: { removed, version, published: !!publish } };
|
|
1073
|
-
} catch (error) {
|
|
1074
|
-
logger.error(error?.message ?? error);
|
|
1075
|
-
return { success: false, error: error?.message ?? String(error) };
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
884
|
const parseApplicationConfigData = (rawConfig) => {
|
|
1080
885
|
if (rawConfig == null) {
|
|
1081
886
|
return null;
|
|
@@ -1222,12 +1027,6 @@ async function addApp({
|
|
|
1222
1027
|
serverMode = "prod",
|
|
1223
1028
|
/* Extra directories whose DOMAIN configs upload alongside this one's (see --include). */
|
|
1224
1029
|
include = [],
|
|
1225
|
-
/* Doc 8 — add the domains to the tenant's application config instead of replacing it (see --register). */
|
|
1226
|
-
register = false,
|
|
1227
|
-
/* Doc 8 (2026-10-10) — the SYS$APPS row this publish writes, from the config being published
|
|
1228
|
-
(applicationConfig.js appMetadataOf): { appName, displayName, menuLayout }. Without it the row is named
|
|
1229
|
-
from the host APPLICATION_CONFIG, as before. */
|
|
1230
|
-
appMeta = null,
|
|
1231
1030
|
} = {}) {
|
|
1232
1031
|
const oldCwd = process.cwd();
|
|
1233
1032
|
|
|
@@ -1270,7 +1069,7 @@ async function addApp({
|
|
|
1270
1069
|
SYS$CONFIG rows, not libs — upload the configs instead of packing a bundle. Guarded on
|
|
1271
1070
|
`body == null` so the publish path (publishApplicationConfig -> publishApp -> addApp with
|
|
1272
1071
|
generated template scripts) can never recurse back into config mode. */
|
|
1273
|
-
if (body == null &&
|
|
1072
|
+
if (body == null && findApplicationConfigFile(mergedSourceFiles)) {
|
|
1274
1073
|
return await uploadConfigs({
|
|
1275
1074
|
workingDir: process.cwd(),
|
|
1276
1075
|
fileNames: mergedSourceFiles,
|
|
@@ -1283,7 +1082,6 @@ async function addApp({
|
|
|
1283
1082
|
forceFull,
|
|
1284
1083
|
releaseCc,
|
|
1285
1084
|
serverMode,
|
|
1286
|
-
register,
|
|
1287
1085
|
});
|
|
1288
1086
|
}
|
|
1289
1087
|
|
|
@@ -1373,14 +1171,13 @@ async function addApp({
|
|
|
1373
1171
|
: `${server}:${apiPort}/${baseUrl}`;
|
|
1374
1172
|
|
|
1375
1173
|
const headers = { "Content-Type": "text/plain" };
|
|
1376
|
-
const { displayName: appDisplayName, menuLayout } =
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
});
|
|
1174
|
+
const { displayName: appDisplayName, menuLayout } =
|
|
1175
|
+
await resolveAppMetadataFromApplicationConfig({
|
|
1176
|
+
server,
|
|
1177
|
+
apiPort,
|
|
1178
|
+
dbIndex,
|
|
1179
|
+
sub,
|
|
1180
|
+
});
|
|
1384
1181
|
const useApplicationConfigName = body != null;
|
|
1385
1182
|
if (useApplicationConfigName && appDisplayName.length > 0) {
|
|
1386
1183
|
keys.metadata["name"] = appDisplayName;
|
|
@@ -1390,11 +1187,9 @@ async function addApp({
|
|
|
1390
1187
|
if (useApplicationConfigName && menuLayout) {
|
|
1391
1188
|
keys.metadata["menuLayout"] = menuLayout;
|
|
1392
1189
|
}
|
|
1393
|
-
const uploadAppName =
|
|
1394
|
-
?
|
|
1395
|
-
|
|
1396
|
-
(useApplicationConfigName ? appDisplayName : "") || bundleName,
|
|
1397
|
-
);
|
|
1190
|
+
const uploadAppName = normalizeAppName(
|
|
1191
|
+
(useApplicationConfigName ? appDisplayName : "") || bundleName,
|
|
1192
|
+
);
|
|
1398
1193
|
/* SYS$APPS is a Biz-A table, not a FINA one, so a PostgreSQL tenant is written
|
|
1399
1194
|
directly — no FINA server in the path at all. On Firebird the FINA stored
|
|
1400
1195
|
procedure TOrmMethod/"setApp" still owns the write. */
|
|
@@ -1517,7 +1312,7 @@ const buildCli = () =>
|
|
|
1517
1312
|
addCommandOptions,
|
|
1518
1313
|
async (commandOptions) => {
|
|
1519
1314
|
/* Config mode uploads SYS$CONFIG rows, not app scripts — skip the jest gate. */
|
|
1520
|
-
if (isConfigModeDir(commandOptions.workingDir, commandOptions.files
|
|
1315
|
+
if (isConfigModeDir(commandOptions.workingDir, commandOptions.files)) {
|
|
1521
1316
|
await addApp({
|
|
1522
1317
|
workingDir: path.resolve(commandOptions.workingDir),
|
|
1523
1318
|
verbose: commandOptions.verbose,
|
|
@@ -1531,7 +1326,6 @@ const buildCli = () =>
|
|
|
1531
1326
|
releaseCc: commandOptions.releaseCc,
|
|
1532
1327
|
serverMode: commandOptions.serverMode,
|
|
1533
1328
|
include: commandOptions.include,
|
|
1534
|
-
register: commandOptions.register,
|
|
1535
1329
|
});
|
|
1536
1330
|
return;
|
|
1537
1331
|
}
|
|
@@ -1592,34 +1386,6 @@ const buildCli = () =>
|
|
|
1592
1386
|
})();
|
|
1593
1387
|
},
|
|
1594
1388
|
)
|
|
1595
|
-
.command(
|
|
1596
|
-
"unregister",
|
|
1597
|
-
"Take domains out of the host application config (Doc 8), e.g. when they move to their own application",
|
|
1598
|
-
(yargs) =>
|
|
1599
|
-
yargs
|
|
1600
|
-
.option("domains", {
|
|
1601
|
-
describe: "Comma-separated domain names to take out",
|
|
1602
|
-
type: "string",
|
|
1603
|
-
demandOption: true,
|
|
1604
|
-
})
|
|
1605
|
-
.option("publish", {
|
|
1606
|
-
describe: "Republish the host application config afterwards",
|
|
1607
|
-
type: "boolean",
|
|
1608
|
-
default: false,
|
|
1609
|
-
}),
|
|
1610
|
-
async (commandOptions) => {
|
|
1611
|
-
const result = await unregisterAppDomains({
|
|
1612
|
-
domains: commandOptions.domains,
|
|
1613
|
-
server: commandOptions.server,
|
|
1614
|
-
apiPort: commandOptions.apiPort,
|
|
1615
|
-
dbIndex: Number(commandOptions.dbIndex),
|
|
1616
|
-
sub: commandOptions.sub,
|
|
1617
|
-
publish: commandOptions.publish,
|
|
1618
|
-
serverMode: commandOptions.serverMode,
|
|
1619
|
-
});
|
|
1620
|
-
if (!result.success) process.exitCode = 1;
|
|
1621
|
-
},
|
|
1622
|
-
)
|
|
1623
1389
|
.command(
|
|
1624
1390
|
"migrate:new",
|
|
1625
1391
|
"Create a new blank migration file with an epoch timestamp",
|
|
@@ -1740,5 +1506,4 @@ export {
|
|
|
1740
1506
|
resolveUpsertBlocks,
|
|
1741
1507
|
uploadConfigs,
|
|
1742
1508
|
isConfigModeDir,
|
|
1743
|
-
unregisterAppDomains,
|
|
1744
1509
|
};
|
package/bin/hub.js
CHANGED
|
@@ -136,13 +136,6 @@ const argv = yargs(process.argv.slice(2))
|
|
|
136
136
|
"Doc 1 0b.6b: base URL of the central AI gateway (biz-a-server /ai) that ccai calls. Defaults to this hub's server (serverMode).",
|
|
137
137
|
type: "string",
|
|
138
138
|
})
|
|
139
|
-
.options("finaCloudDbindex", {
|
|
140
|
-
describe:
|
|
141
|
-
"Doc 8: the FINA API database FINA Cloud works on (FINA's own index, not Biz-A's). Absent = FINA Cloud off. When set, browser writes to that database are refused: they go through engine/fina.",
|
|
142
|
-
type: "number",
|
|
143
|
-
/* ⚠️ No default, and an operator setting rather than a console one: one FINA API can serve several
|
|
144
|
-
companies, and the index chooses whose books these are. */
|
|
145
|
-
})
|
|
146
139
|
.options("services", {
|
|
147
140
|
describe:
|
|
148
141
|
"Comma-separated list of engine jobs this agent handles (e.g., 'BPM,EMAIL')",
|
|
@@ -210,7 +203,6 @@ import { createExtRouter } from "../engine/ext/routes.js";
|
|
|
210
203
|
import { createLiveExtDeps } from "../engine/ext/live.js";
|
|
211
204
|
import axios from "axios";
|
|
212
205
|
import { createFinaRouter } from "../engine/orm/finaEndpoint.js";
|
|
213
|
-
import { createLiveFinaReadPolicy } from "../engine/fina/readPolicy.js";
|
|
214
206
|
import { createSessionStore } from "../engine/orm/sessionStore.js";
|
|
215
207
|
import { resolveUserTenancy } from "../engine/orm/tenantResolution.js";
|
|
216
208
|
import { buildUserTenantSql } from "../engine/orm/finaAuth.js";
|
|
@@ -340,14 +332,6 @@ app.use(
|
|
|
340
332
|
/* A v2 save that creates a record of an entity declaring its workflow starts it — the same
|
|
341
333
|
starter the socket entrance (bin/hubEvent.js) hands the router. */
|
|
342
334
|
startWorkflow: startWorkflowOnCreate,
|
|
343
|
-
/* Doc 8 D2 — the FINA Cloud write guard, the same one the socket entrance runs (bin/hubEvent.js). */
|
|
344
|
-
finaCloudDbIndex: argv.finaCloudDbindex,
|
|
345
|
-
/* Doc 8 M8 — and the read policy (a mapped FINA user), as the socket entrance runs it. */
|
|
346
|
-
finaReadPolicy: createLiveFinaReadPolicy({
|
|
347
|
-
argv,
|
|
348
|
-
isPostgresIndex,
|
|
349
|
-
readSession: hubSessionStore ? (sid) => hubSessionStore.read(sid) : null,
|
|
350
|
-
}),
|
|
351
335
|
/* So a locally-served response still tells the client where to open its sockets — the
|
|
352
336
|
tunnel's byte-level injection only fires for real FINA responses. */
|
|
353
337
|
cliAddress: () => (argv.cliAddress ? argv.cliAddress() : {}),
|
package/bin/hubEvent.js
CHANGED
|
@@ -21,9 +21,6 @@ import { tenantSqlFor } from "../engine/orm/tenantSql.js";
|
|
|
21
21
|
import { ensureEntityConfig } from "../engine/domain/entityConfigPrimer.js";
|
|
22
22
|
import { startWorkflowOnCreate } from "../engine/bpm/workflowOnCreate.js";
|
|
23
23
|
import { bpmContextFromHeaders } from "../engine/bpm/bpmContext.js";
|
|
24
|
-
import { finaContext } from "../engine/fina/context.js";
|
|
25
|
-
import { refuseFinaCloudWrite } from "../engine/fina/guard.js";
|
|
26
|
-
import { createLiveFinaReadPolicy } from "../engine/fina/readPolicy.js";
|
|
27
24
|
import { runAsPrincipal, runTrusted, ACTIVE_TENANT_NOT_HELD } from "../engine/orm/tenantContext.js";
|
|
28
25
|
import { resolveRequestScope, SESSION_NOT_USABLE } from "../engine/orm/requestScope.js";
|
|
29
26
|
import { createSessionStore } from "../engine/orm/sessionStore.js";
|
|
@@ -54,7 +51,6 @@ import {
|
|
|
54
51
|
writeCompanyLogo,
|
|
55
52
|
clearCompanyLogo,
|
|
56
53
|
} from "../engine/orm/companyLogo.js";
|
|
57
|
-
import { readCompanyLocale, writeCompanyLocale } from "../engine/orm/companyLocale.js";
|
|
58
54
|
import {
|
|
59
55
|
execPostgres,
|
|
60
56
|
execPostgresStatements,
|
|
@@ -96,23 +92,6 @@ const sessionStoreFor = (dbIndex) => {
|
|
|
96
92
|
}
|
|
97
93
|
return sessionStores.get(dbIndex);
|
|
98
94
|
};
|
|
99
|
-
/* Doc 8 M8 — the FINA read policy (S-02: reading FINA takes a mapped FINA user), one per hub, built on first use from
|
|
100
|
-
the hub's own argv and session store: the same context the `fina/*` engine routes get. */
|
|
101
|
-
let finaReadPolicyOfHub = null;
|
|
102
|
-
const finaReadPolicyFor = (argv) => {
|
|
103
|
-
if (!finaReadPolicyOfHub) {
|
|
104
|
-
finaReadPolicyOfHub = createLiveFinaReadPolicy({
|
|
105
|
-
argv,
|
|
106
|
-
isPostgresIndex,
|
|
107
|
-
readSession: (sid) => {
|
|
108
|
-
const store = sessionStoreFor(Number(argv["dbindex"]));
|
|
109
|
-
return store ? store.read(sid) : null;
|
|
110
|
-
},
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
return finaReadPolicyOfHub;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
95
|
const openSessionFor = (dbIndex) => {
|
|
117
96
|
const store = sessionStoreFor(dbIndex);
|
|
118
97
|
return store ? (args) => store.open(args) : null;
|
|
@@ -521,21 +500,6 @@ const EngineRegistry = {
|
|
|
521
500
|
"domain/appConfig": () => import("../engine/domain/appConfig.js"),
|
|
522
501
|
"domain/applicationConfig": () =>
|
|
523
502
|
import("../engine/domain/applicationConfig.js"),
|
|
524
|
-
/* Doc 8 — FINA Cloud. ⚠️ Route files only: every export of a registered module is callable from a browser. */
|
|
525
|
-
"fina/setup": () => import("../engine/fina/routes/setup.js"),
|
|
526
|
-
"fina/session": () => import("../engine/fina/routes/session.js"),
|
|
527
|
-
"fina/master": () => import("../engine/fina/routes/master.js"),
|
|
528
|
-
/* Doc 8 M4 — the template engine's server side. */
|
|
529
|
-
"fina/template": () => import("../engine/fina/routes/template.js"),
|
|
530
|
-
"fina/document": () => import("../engine/fina/routes/document.js"),
|
|
531
|
-
"fina/print": () => import("../engine/fina/routes/print.js"),
|
|
532
|
-
"fina/recurring": () => import("../engine/fina/routes/recurring.js"),
|
|
533
|
-
/* Doc 8 M5b — which months FINA keeps open, for the rows a list shows (L-08). */
|
|
534
|
-
"fina/period": () => import("../engine/fina/routes/period.js"),
|
|
535
|
-
/* Doc 8 M6 — a FINA document's attachments, kept in Biz-A (SYS$ATTACHMENT). */
|
|
536
|
-
"fina/attachment": () => import("../engine/fina/routes/attachment.js"),
|
|
537
|
-
/* Doc 8 M7 — JV submissions (pengajuan) with Need Approval on; the approval workflow posts them. */
|
|
538
|
-
"fina/submission": () => import("../engine/fina/routes/submission.js"),
|
|
539
503
|
};
|
|
540
504
|
|
|
541
505
|
/* Doc 1 step 3 — what a Phase B turn needs: the draft store, the gap list, a draft opener that applies the
|
|
@@ -701,30 +665,6 @@ const handleCliCommand = async (data, cb, argv) => {
|
|
|
701
665
|
}
|
|
702
666
|
break;
|
|
703
667
|
}
|
|
704
|
-
/*
|
|
705
|
-
* Agung 2026-10-10 — the company number format (id / en-US), the theme's path: read by anyone signed
|
|
706
|
-
* in (every screen formats numbers by it), written only by a verified administrator
|
|
707
|
-
* (companyLocale.js), audited. A refused value is an answer with `problems`, not an error.
|
|
708
|
-
*/
|
|
709
|
-
case "companylocale": {
|
|
710
|
-
const localePrincipal = principalForCliScript(data.headers, {
|
|
711
|
-
mode: argv["principalMode"] || "observe",
|
|
712
|
-
});
|
|
713
|
-
const localeDb = tenantSqlFor(argv["dbindex"]);
|
|
714
|
-
if (data.action === "write") {
|
|
715
|
-
const saved = await writeCompanyLocale({
|
|
716
|
-
exec: localeDb.exec,
|
|
717
|
-
execStatements: localeDb.atomic,
|
|
718
|
-
sql: localeDb,
|
|
719
|
-
principal: localePrincipal,
|
|
720
|
-
locale: data.locale,
|
|
721
|
-
});
|
|
722
|
-
cb(null, { success: saved.ok, problems: saved.problems, data: saved.locale });
|
|
723
|
-
} else {
|
|
724
|
-
cb(null, { success: true, data: await readCompanyLocale({ exec: localeDb.exec }) });
|
|
725
|
-
}
|
|
726
|
-
break;
|
|
727
|
-
}
|
|
728
668
|
/*
|
|
729
669
|
* Doc 6 §8.2 — the company logo, under its OWN sys$config key.
|
|
730
670
|
*
|
|
@@ -906,23 +846,12 @@ const handleCliCommand = async (data, cb, argv) => {
|
|
|
906
846
|
* These are engine operations invoked by an administrator, not tenant data reads, so
|
|
907
847
|
* they are trusted for the same reason everything behind db/db.js is.
|
|
908
848
|
*/
|
|
909
|
-
/*
|
|
910
|
-
|
|
911
|
-
(bpm/bpmContext.js), and FINA Cloud, whose every route acts as the verified user (Doc 8 D2,
|
|
912
|
-
engine/fina/context.js). */
|
|
849
|
+
/* The BPM runtime alone gets who is asking — the signed principal in `data.headers`, never the
|
|
850
|
+
caller-shaped payload — for transitions that ask for verified roles (bpm/bpmContext.js). */
|
|
913
851
|
const engineContext =
|
|
914
852
|
modulePath === "bpm/workflowRT"
|
|
915
853
|
? bpmContextFromHeaders(data.headers, { mode: argv["principalMode"] || "observe" })
|
|
916
|
-
:
|
|
917
|
-
? await finaContext(data.headers, {
|
|
918
|
-
argv,
|
|
919
|
-
isPostgresIndex,
|
|
920
|
-
readSession: (() => {
|
|
921
|
-
const store = sessionStoreFor(Number(argv["dbindex"]));
|
|
922
|
-
return store ? (sid) => store.read(sid) : null;
|
|
923
|
-
})(),
|
|
924
|
-
})
|
|
925
|
-
: undefined;
|
|
854
|
+
: undefined;
|
|
926
855
|
const result = await runTrusted(() =>
|
|
927
856
|
engineContext
|
|
928
857
|
? engineModule[methodName](data.payload, argv, engineContext)
|
|
@@ -1059,28 +988,7 @@ export const clientListener = (socket, argv, via = "?") => {
|
|
|
1059
988
|
: undefined,
|
|
1060
989
|
});
|
|
1061
990
|
if (argv["subdomain"].localeCompare(reqData.subDomain) == 0) {
|
|
1062
|
-
|
|
1063
|
-
checking the user's FINA rights and the date lock. Checked HERE, the one place this transport
|
|
1064
|
-
hands a request to FINA; the HTTP entrance runs the same guard (finaEndpoint.js). */
|
|
1065
|
-
/* Doc 8 M8 — and READING FINA takes a Biz-A user mapped to a FINA user (S-02, engine/fina/readPolicy.js). */
|
|
1066
|
-
const finaReadPolicy = finaReadPolicyFor(argv);
|
|
1067
|
-
const forwardToApi = async () => {
|
|
1068
|
-
const refused =
|
|
1069
|
-
refuseFinaCloudWrite({ path: reqData.path, body: reqBody, finaCloudDbIndex: argv["finaCloudDbindex"] }) ??
|
|
1070
|
-
(await finaReadPolicy({ path: reqData.path, body: reqBody, headers: reqData.headers }));
|
|
1071
|
-
if (refused) {
|
|
1072
|
-
logger.warn(`[apiRequest] refused ${reqData.path}: ${refused.body.error}`);
|
|
1073
|
-
return resCB(null, {
|
|
1074
|
-
status: refused.status,
|
|
1075
|
-
statusText: "Forbidden",
|
|
1076
|
-
headers: { "content-type": "application/json", ...cliAddressHeaders() },
|
|
1077
|
-
body: refused.body,
|
|
1078
|
-
url: apiAddress + reqData.path,
|
|
1079
|
-
});
|
|
1080
|
-
}
|
|
1081
|
-
return forwardToFina();
|
|
1082
|
-
};
|
|
1083
|
-
const forwardToFina = () =>
|
|
991
|
+
const forwardToApi = () =>
|
|
1084
992
|
axios
|
|
1085
993
|
.request({
|
|
1086
994
|
timeout:
|
|
@@ -1208,8 +1116,7 @@ export const clientListener = (socket, argv, via = "?") => {
|
|
|
1208
1116
|
status: routed.status,
|
|
1209
1117
|
statusText: routed.status === 200 ? "OK" : "Error",
|
|
1210
1118
|
headers: {
|
|
1211
|
-
|
|
1212
|
-
"content-type": routed.contentType ?? "application/json",
|
|
1119
|
+
"content-type": "application/json",
|
|
1213
1120
|
...cliAddressHeaders(),
|
|
1214
1121
|
},
|
|
1215
1122
|
body: routed.body,
|
package/bin/tunnelTarget.js
CHANGED
|
@@ -48,10 +48,6 @@ export const resolveTunnelTarget = (argv, isPostgresIndex, firstChunk) => {
|
|
|
48
48
|
};
|
|
49
49
|
|
|
50
50
|
if (isLoginRequest(firstChunk)) return ownEndpoint(argv);
|
|
51
|
-
/* Doc 8 D2 — with FINA Cloud on, everything loops back: the FINA Cloud write guard sits on the CLI's own
|
|
52
|
-
endpoint (finaEndpoint.js), and a byte pipe straight to FINA would pass it by. */
|
|
53
|
-
const finaCloud = Number(argv.finaCloudDbindex);
|
|
54
|
-
if (Number.isInteger(finaCloud) && finaCloud > 0) return ownEndpoint(argv);
|
|
55
51
|
if (!argv.dbindex) return fina;
|
|
56
52
|
|
|
57
53
|
let servedHere = false;
|
package/engine/bpm/workflow.js
CHANGED
|
@@ -4,7 +4,6 @@ import { logger } from "../../logger.js";
|
|
|
4
4
|
import { compileDefinition } from "./rules.js";
|
|
5
5
|
import { validateWorkflow } from "./validateWorkflow.js";
|
|
6
6
|
import { ruleModeOf } from "./domainRules.js";
|
|
7
|
-
import { afterCommit } from "../domain/dialect.js";
|
|
8
7
|
|
|
9
8
|
async function processUpsertResult(requesterPayload, apiConfig, result) {
|
|
10
9
|
if (result.success == "OK") {
|
|
@@ -27,12 +26,12 @@ async function processUpsertResult(requesterPayload, apiConfig, result) {
|
|
|
27
26
|
apiConfig,
|
|
28
27
|
);
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
ClientEmitter.broadcastClient(
|
|
30
|
+
"SYS$BPM_WORKFLOW_DEF_UPSERT",
|
|
31
|
+
newRecord,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
ClientEmitter.notifyRequester(requesterPayload, newRecord);
|
|
36
35
|
}
|
|
37
36
|
} catch (esbError) {
|
|
38
37
|
logger.error(
|