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
|
@@ -6,47 +6,11 @@ import {
|
|
|
6
6
|
buildPublishStatusConfigKey,
|
|
7
7
|
writePublishStatus,
|
|
8
8
|
saveConfigRow,
|
|
9
|
-
appConfigNameFor,
|
|
10
9
|
} from "./appConfig.js";
|
|
11
10
|
import { execPostgres } from "./dialect.js";
|
|
12
11
|
/* Dok. 5 section 3.3 -- a Layer 3 domain contributing its own menus, gated by its own activation
|
|
13
12
|
setting. Read generically: no domain name and no domain code appear in this file. */
|
|
14
|
-
import { mergeDomainNavigation, selectActiveDomainMenus, readActiveModules
|
|
15
|
-
import { entryPointsOf } from "../cc/appMenus.js";
|
|
16
|
-
|
|
17
|
-
/*
|
|
18
|
-
* Doc 8 (2026-10-10) — a SECONDARY application (`metadata.app`, stored as `APPLICATION_CONFIG.<app>`) beside the
|
|
19
|
-
* tenant's host app. It has no Home of its own: `./main` is the tenant's, and the client loads `/main` by bare name
|
|
20
|
-
* across every app, so a second app's main.js could take over the tenant's Home.
|
|
21
|
-
*/
|
|
22
|
-
const isSecondaryApp = (parsedApplicationConfig) => {
|
|
23
|
-
const app = parsedApplicationConfig?.metadata?.app;
|
|
24
|
-
return app != null && app !== "";
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/* Mirrors bin/app.js normalizeAppName: how the host's display name keys its SYS$APPS row. */
|
|
28
|
-
const hostAppNameOf = (displayName) =>
|
|
29
|
-
String(displayName ?? "")
|
|
30
|
-
.trim()
|
|
31
|
-
.replace(/\s+/g, "")
|
|
32
|
-
.replace(/-/g, "")
|
|
33
|
-
.toLowerCase();
|
|
34
|
-
|
|
35
|
-
/*
|
|
36
|
-
* The SYS$APPS row this config publishes to: its name, the display name the client shows on the app's menu node, and
|
|
37
|
-
* the menu layout. Taken from the config BEING published — before this, addApp read them from the newest host
|
|
38
|
-
* APPLICATION_CONFIG whatever was published, so a second app would have overwritten the host's row.
|
|
39
|
-
*/
|
|
40
|
-
export const appMetadataOf = (parsedApplicationConfig) => {
|
|
41
|
-
const metadata = parsedApplicationConfig?.metadata ?? {};
|
|
42
|
-
const displayName = typeof metadata.name === "string" ? metadata.name.trim() : "";
|
|
43
|
-
const configName = appConfigNameFor(metadata);
|
|
44
|
-
return {
|
|
45
|
-
appName: isSecondaryApp(parsedApplicationConfig) ? configName.slice(configName.indexOf(".") + 1) : hostAppNameOf(displayName),
|
|
46
|
-
displayName,
|
|
47
|
-
menuLayout: resolveMenuLayout(parsedApplicationConfig),
|
|
48
|
-
};
|
|
49
|
-
};
|
|
13
|
+
import { mergeDomainNavigation, selectActiveDomainMenus, readActiveModules } from "./domainNavigation.js";
|
|
50
14
|
|
|
51
15
|
export const parseConfigData = (rawConfig) => {
|
|
52
16
|
if (rawConfig == null) {
|
|
@@ -123,7 +87,6 @@ export const buildMenuStructureFromApplicationConfig = (parsedApplicationConfig)
|
|
|
123
87
|
(menu) => !(menu?.caption === "Home" || (Array.isArray(menu?.link) && menu.link[0] === "./main")),
|
|
124
88
|
);
|
|
125
89
|
|
|
126
|
-
if (isSecondaryApp(parsedApplicationConfig)) return nonHomeMenus;
|
|
127
90
|
return [{ menuName: "", caption: "Home", link: ["./main"], subMenu: [] }].concat(nonHomeMenus);
|
|
128
91
|
};
|
|
129
92
|
|
|
@@ -135,20 +98,6 @@ export const normalizeTemplateFileName = (rawName) => {
|
|
|
135
98
|
return normalized.endsWith(".js") ? normalized : `${normalized}.js`;
|
|
136
99
|
};
|
|
137
100
|
|
|
138
|
-
/*
|
|
139
|
-
* Fold each domain's live menus into the application's navigation (domainNavigation.js), with the TYPES of that
|
|
140
|
-
* domain's own entry points — Doc 8 M3: a leaf on a list entry point is a list link, so it opens the list and the
|
|
141
|
-
* publish builds a list stub for it (collectFormTargets reads "./list"). `domainMenus`: [domainKey, {menus,
|
|
142
|
-
* domainConfig}] pairs, a Map or an array.
|
|
143
|
-
*/
|
|
144
|
-
export const foldDomainNavigation = (navigation, domainMenus) => {
|
|
145
|
-
let folded = navigation ?? {};
|
|
146
|
-
for (const [domainKey, { menus, domainConfig }] of domainMenus) {
|
|
147
|
-
folded = mergeDomainNavigation({ applicationConfig: { navigation: folded }, domainKey, menus, entryPointTypes: entryPointsOf(domainConfig) });
|
|
148
|
-
}
|
|
149
|
-
return folded;
|
|
150
|
-
};
|
|
151
|
-
|
|
152
101
|
export const collectFormTargets = (menuNode, inheritedDomainKey = null) => {
|
|
153
102
|
const targets = [];
|
|
154
103
|
const currentDomainKey =
|
|
@@ -225,8 +174,7 @@ export const resolveDomainKeyByReference = (domainReference, domainMap) => {
|
|
|
225
174
|
export const resolveTemplateTargets = (parsedDomainConfig, parsedApplicationConfig) => {
|
|
226
175
|
const sourceMenus = Array.isArray(parsedApplicationConfig?.navigation?.menus) ? parsedApplicationConfig.navigation.menus : [];
|
|
227
176
|
const resolvedTargets = [];
|
|
228
|
-
|
|
229
|
-
const seenFileNames = new Set(isSecondaryApp(parsedApplicationConfig) ? ["main.js"] : []);
|
|
177
|
+
const seenFileNames = new Set();
|
|
230
178
|
|
|
231
179
|
for (const menuNode of sourceMenus) {
|
|
232
180
|
const menuTargets = collectFormTargets(menuNode);
|
|
@@ -536,9 +484,8 @@ ${sharedDomainConfigResolverScript}
|
|
|
536
484
|
ensureDomainConfig((resolvedConfig) => {
|
|
537
485
|
this.domainConfig = resolvedConfig || this.domainConfig;
|
|
538
486
|
console.log('Template Result :', this.domainConfig);
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
Promise.resolve(waited).then(() => resolve(), () => resolve());
|
|
487
|
+
this.domainService.doBeforeLoadDataEvent({ config: this.domainConfig, entryPointName: "__ENTRY_POINT_NAME__", runtimeHost: this, id: String(id) });
|
|
488
|
+
resolve();
|
|
542
489
|
});
|
|
543
490
|
});
|
|
544
491
|
},
|
|
@@ -597,10 +544,7 @@ ${sharedDomainConfigResolverScript}
|
|
|
597
544
|
ensureDomainConfig((resolvedConfig) => {
|
|
598
545
|
this.domainConfig = resolvedConfig || this.domainConfig;
|
|
599
546
|
console.log('Template Result :', this.domainConfig);
|
|
600
|
-
|
|
601
|
-
database index in one, and the first load must not read the tenant's database instead. An older
|
|
602
|
-
client answers nothing; a failed handler does not stop the list loading. */
|
|
603
|
-
const waited = this.domainService.doBeforeLoadDataEvent({
|
|
547
|
+
this.domainService.doBeforeLoadDataEvent({
|
|
604
548
|
config: this.domainConfig,
|
|
605
549
|
entryPointName: "__ENTRY_POINT_NAME__",
|
|
606
550
|
runtimeHost: this,
|
|
@@ -622,21 +566,13 @@ ${sharedDomainConfigResolverScript}
|
|
|
622
566
|
this.buttons = rendered.buttons || [];
|
|
623
567
|
this.filter = rendered.filter || [];
|
|
624
568
|
this.distinct = !!rendered.distinct;
|
|
625
|
-
/* Doc 8 M2: the database a list reads (a FINA list reads FINA), and the list engine options
|
|
626
|
-
(search, print, selection) - handed over whole; an older client has no applyListOptions. */
|
|
627
|
-
if (rendered.dbIndex !== undefined && rendered.dbIndex !== null) {
|
|
628
|
-
this.dbIndex = rendered.dbIndex;
|
|
629
|
-
}
|
|
630
|
-
if (typeof this.applyListOptions === "function") {
|
|
631
|
-
this.applyListOptions(rendered);
|
|
632
|
-
}
|
|
633
569
|
this.functions = Object.assign(this.functions || {}, rendered.functions || {});
|
|
634
570
|
if (this.helper && typeof this.helper.bindFunctionInObj === "function") {
|
|
635
571
|
this.helper.bindFunctionInObj(this.functions, this);
|
|
636
572
|
}
|
|
637
573
|
this._listDomainInitialized = true;
|
|
638
574
|
}
|
|
639
|
-
|
|
575
|
+
resolve();
|
|
640
576
|
});
|
|
641
577
|
});
|
|
642
578
|
},
|
|
@@ -823,15 +759,6 @@ export const publishApplicationConfig = async (payload = {}, argv = {}) => {
|
|
|
823
759
|
throw new Error(`SYS$CONFIG record ${applicationConfigName}@${applicationConfigVersion} was not found.`);
|
|
824
760
|
}
|
|
825
761
|
let parsedApplicationConfig = parseConfigData(appRow.data) ?? {};
|
|
826
|
-
/* Doc 8 (2026-10-10): the row's name and the app the config declares must agree — otherwise one app's config would
|
|
827
|
-
publish into another app's SYS$APPS row. */
|
|
828
|
-
const declaredConfigName = appConfigNameFor(parsedApplicationConfig?.metadata);
|
|
829
|
-
if (declaredConfigName.toUpperCase() !== applicationConfigName.toUpperCase()) {
|
|
830
|
-
throw new Error(
|
|
831
|
-
`SYS$CONFIG record ${applicationConfigName}@${applicationConfigVersion} declares itself as ${declaredConfigName} ` +
|
|
832
|
-
"(metadata.app); publish it under that name.",
|
|
833
|
-
);
|
|
834
|
-
}
|
|
835
762
|
|
|
836
763
|
/* An app bundle publish is a full replace (addApp() rebuilds the whole bundle from scratch every call), so
|
|
837
764
|
template.js scaffolding must be generated for EVERY domain registered in application.js's `domains` map, not
|
|
@@ -923,10 +850,7 @@ export const publishApplicationConfig = async (payload = {}, argv = {}) => {
|
|
|
923
850
|
exec: execPostgres,
|
|
924
851
|
dbIndex: Number(config?.dbindex ?? config?.dbIndex),
|
|
925
852
|
});
|
|
926
|
-
domainNavigation.set(domainKey, {
|
|
927
|
-
menus: selectActiveDomainMenus({ menus: declaredMenus, activeModules }),
|
|
928
|
-
domainConfig: parsedDomainConfig,
|
|
929
|
-
});
|
|
853
|
+
domainNavigation.set(domainKey, selectActiveDomainMenus({ menus: declaredMenus, activeModules }));
|
|
930
854
|
} catch (error) {
|
|
931
855
|
logger.warn(
|
|
932
856
|
`[navigation] ${domainKey}: could not resolve its menus, so it contributes none: ` +
|
|
@@ -938,7 +862,10 @@ export const publishApplicationConfig = async (payload = {}, argv = {}) => {
|
|
|
938
862
|
|
|
939
863
|
/* Fold the domains' menus in BEFORE the bundle is built from this config. */
|
|
940
864
|
if (domainNavigation.size > 0) {
|
|
941
|
-
|
|
865
|
+
let navigation = parsedApplicationConfig?.navigation ?? {};
|
|
866
|
+
for (const [domainKey, menus] of domainNavigation) {
|
|
867
|
+
navigation = mergeDomainNavigation({ applicationConfig: { navigation }, domainKey, menus });
|
|
868
|
+
}
|
|
942
869
|
parsedApplicationConfig = { ...parsedApplicationConfig, navigation };
|
|
943
870
|
}
|
|
944
871
|
|
|
@@ -951,8 +878,6 @@ export const publishApplicationConfig = async (payload = {}, argv = {}) => {
|
|
|
951
878
|
const publishBody = {
|
|
952
879
|
verbose: false,
|
|
953
880
|
fileList: mergedFileList.map(({ name, content }) => ({ name, content })),
|
|
954
|
-
/* Doc 8 (2026-10-10): the SYS$APPS row is the one THIS config names (addApp's appMeta). */
|
|
955
|
-
appMeta: appMetadataOf(parsedApplicationConfig),
|
|
956
881
|
/* Doc 1 0b.3: addApp bundles the working dir's own files too; a caller inside the long-lived hub
|
|
957
882
|
(ccpublish) passes an empty scratch dir instead of letting it use process.cwd(). */
|
|
958
883
|
...(payload?.workingDir ? { workingDir: payload.workingDir } : {}),
|
|
@@ -988,7 +913,7 @@ export const publishApplicationConfig = async (payload = {}, argv = {}) => {
|
|
|
988
913
|
`get = function () { return ${JSON.stringify(parsedApplicationConfig)}; }`,
|
|
989
914
|
config,
|
|
990
915
|
);
|
|
991
|
-
const counts = [...domainNavigation].map(([key,
|
|
916
|
+
const counts = [...domainNavigation].map(([key, menus]) => `${key}: ${menus.length}`).join(", ");
|
|
992
917
|
logger.info(`[navigation] domain menus published (${counts})`);
|
|
993
918
|
} catch (error) {
|
|
994
919
|
/* The bundle is already published and correct; a menu that did not update is a re-publish
|
package/engine/domain/dialect.js
CHANGED
|
@@ -13,8 +13,6 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
17
|
-
import { logger } from "../../logger.js";
|
|
18
16
|
import * as firebirdDdl from "./firebird-ddl.js";
|
|
19
17
|
import * as postgresDdl from "./postgres-ddl.js";
|
|
20
18
|
import {
|
|
@@ -166,9 +164,6 @@ const decorate = (error, statement) => {
|
|
|
166
164
|
* anything at all.
|
|
167
165
|
*/
|
|
168
166
|
export const execPostgres = async (sql, dbIndex) => {
|
|
169
|
-
const ambient = openAmbient(dbIndex);
|
|
170
|
-
if (ambient) return inSavepoint(ambient, () => runOn(ambient.client, sql), () => execPostgres(sql, dbIndex));
|
|
171
|
-
|
|
172
167
|
const pool = await poolFactory(dbIndex);
|
|
173
168
|
|
|
174
169
|
if (!hasTenantContext()) {
|
|
@@ -217,20 +212,6 @@ const lowerCaseKeys = (rows) =>
|
|
|
217
212
|
write puts the master first and callers read `data[0].ID` as the master's key
|
|
218
213
|
(engine/orm/executor.js runStatementsKeepingFirst). One implementation, two shapes. */
|
|
219
214
|
export const execPostgresStatements = async (statements, dbIndex) => {
|
|
220
|
-
/* Inside a held transaction the group is still atomic — one savepoint around it — but opens no BEGIN of its own. */
|
|
221
|
-
const ambient = openAmbient(dbIndex);
|
|
222
|
-
if (ambient) {
|
|
223
|
-
return inSavepoint(
|
|
224
|
-
ambient,
|
|
225
|
-
async () => {
|
|
226
|
-
const rowsets = [];
|
|
227
|
-
for (const statement of statements ?? []) rowsets.push(await runOn(ambient.client, statement));
|
|
228
|
-
return rowsets;
|
|
229
|
-
},
|
|
230
|
-
() => execPostgresStatements(statements, dbIndex),
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
215
|
const pool = await poolFactory(dbIndex);
|
|
235
216
|
const client = await pool.connect();
|
|
236
217
|
const rowsets = [];
|
|
@@ -281,7 +262,6 @@ export const beginPostgresTransaction = async (
|
|
|
281
262
|
dbIndex,
|
|
282
263
|
{ isolation = "READ COMMITTED", tenantId } = {},
|
|
283
264
|
) => {
|
|
284
|
-
refuseNesting(dbIndex);
|
|
285
265
|
const pool = await poolFactory(dbIndex);
|
|
286
266
|
const client = await pool.connect();
|
|
287
267
|
|
|
@@ -341,121 +321,3 @@ export const beginPostgresTransaction = async (
|
|
|
341
321
|
release,
|
|
342
322
|
};
|
|
343
323
|
};
|
|
344
|
-
|
|
345
|
-
/*
|
|
346
|
-
* Atomic publish (Agung 2026-10-08) — a transaction the engine's ordinary calls JOIN.
|
|
347
|
-
*
|
|
348
|
-
* A Control Center publish runs through ~20 functions (config rows, DDL, hash rows, SYS$APPS, workflows, draft marks)
|
|
349
|
-
* that call `execPostgres` / `execPostgresStatements`, each its own auto-committed statement. A failure part-way left
|
|
350
|
-
* the earlier steps in place (marketing@0.1.13 recorded after its DDL failed). Inside `withPostgresTransaction` those
|
|
351
|
-
* same calls run on ONE held client, each in its own SAVEPOINT — so code that catches a failure and carries on (the
|
|
352
|
-
* DDL retry loop, a 23505 → null) still can — and nothing is visible to anyone until the COMMIT. Calls are queued:
|
|
353
|
-
* `loadTableSnapshot` fires eight at once, and interleaved savepoints would release each other.
|
|
354
|
-
*
|
|
355
|
-
* ⚠️ Only PostgreSQL DDL is transactional; nothing here runs on Firebird. A table locked by a long user query fails
|
|
356
|
-
* the transaction after `lockTimeout` instead of hanging it — and everyone queued behind its locks.
|
|
357
|
-
*/
|
|
358
|
-
const ambientTransactions = new AsyncLocalStorage();
|
|
359
|
-
const LOCK_TIMEOUT = /^\d+(ms|s|min)?$/;
|
|
360
|
-
|
|
361
|
-
const openAmbient = (dbIndex) => {
|
|
362
|
-
const store = ambientTransactions.getStore();
|
|
363
|
-
return store && !store.closed && String(store.dbIndex) === String(dbIndex) ? store : null;
|
|
364
|
-
};
|
|
365
|
-
|
|
366
|
-
const refuseNesting = (dbIndex) => {
|
|
367
|
-
if (openAmbient(dbIndex)) throw new Error(`A transaction is already open on database ${dbIndex}; this work runs inside it.`);
|
|
368
|
-
};
|
|
369
|
-
|
|
370
|
-
const runOn = async (client, sql) => {
|
|
371
|
-
try {
|
|
372
|
-
const result = await client.query(sql);
|
|
373
|
-
return lowerCaseKeys(result?.rows);
|
|
374
|
-
} catch (error) {
|
|
375
|
-
throw decorate(error, sql);
|
|
376
|
-
}
|
|
377
|
-
};
|
|
378
|
-
|
|
379
|
-
/* One call = one savepoint, queued behind the calls before it. A call that only gets its turn after the transaction
|
|
380
|
-
closed (async work outliving it) runs on the pool, as it would have outside. */
|
|
381
|
-
const inSavepoint = (store, work, outside) => {
|
|
382
|
-
const task = async () => {
|
|
383
|
-
if (store.closed) return outside();
|
|
384
|
-
const name = `biza_sp_${++store.savepoints}`;
|
|
385
|
-
await store.client.query(`SAVEPOINT ${name}`);
|
|
386
|
-
try {
|
|
387
|
-
const out = await work();
|
|
388
|
-
await store.client.query(`RELEASE SAVEPOINT ${name}`);
|
|
389
|
-
return out;
|
|
390
|
-
} catch (error) {
|
|
391
|
-
try {
|
|
392
|
-
await store.client.query(`ROLLBACK TO SAVEPOINT ${name}`);
|
|
393
|
-
} catch {
|
|
394
|
-
/* The transaction is unusable anyway; the original error is the one that matters. */
|
|
395
|
-
}
|
|
396
|
-
throw error;
|
|
397
|
-
}
|
|
398
|
-
};
|
|
399
|
-
const result = store.queue.then(task, task);
|
|
400
|
-
store.queue = result.catch(() => {});
|
|
401
|
-
return result;
|
|
402
|
-
};
|
|
403
|
-
|
|
404
|
-
/* Run `fn` in one transaction on `dbIndex`: COMMIT when it returns, ROLLBACK when it throws (the error comes through).
|
|
405
|
-
`afterCommit` work queued inside runs only after a successful COMMIT. */
|
|
406
|
-
export const withPostgresTransaction = async (dbIndex, fn, { lockTimeout = "10s" } = {}) => {
|
|
407
|
-
refuseNesting(dbIndex);
|
|
408
|
-
if (lockTimeout !== null && !LOCK_TIMEOUT.test(String(lockTimeout))) throw new Error(`The lock timeout is a duration like "10s", not ${JSON.stringify(lockTimeout)}.`);
|
|
409
|
-
const pool = await poolFactory(dbIndex);
|
|
410
|
-
const client = await pool.connect();
|
|
411
|
-
const store = { dbIndex, client, queue: Promise.resolve(), savepoints: 0, afterCommit: [], closed: false };
|
|
412
|
-
|
|
413
|
-
try {
|
|
414
|
-
await client.query("BEGIN");
|
|
415
|
-
} catch (error) {
|
|
416
|
-
/* No ROLLBACK: BEGIN failed, so there is nothing to roll back. */
|
|
417
|
-
client.release();
|
|
418
|
-
throw decorate(error, "BEGIN");
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
let out;
|
|
422
|
-
try {
|
|
423
|
-
if (lockTimeout !== null) await client.query(`SET LOCAL lock_timeout = '${lockTimeout}'`);
|
|
424
|
-
await tenantContextApplier(client, { dbIndex });
|
|
425
|
-
out = await ambientTransactions.run(store, fn);
|
|
426
|
-
/* Work started inside and not awaited still belongs to this transaction. */
|
|
427
|
-
await store.queue;
|
|
428
|
-
await client.query("COMMIT");
|
|
429
|
-
} catch (error) {
|
|
430
|
-
store.closed = true;
|
|
431
|
-
try {
|
|
432
|
-
await client.query("ROLLBACK");
|
|
433
|
-
} catch {
|
|
434
|
-
/* Best-effort: the error that got us here is the useful one. */
|
|
435
|
-
}
|
|
436
|
-
client.release();
|
|
437
|
-
throw error;
|
|
438
|
-
}
|
|
439
|
-
store.closed = true;
|
|
440
|
-
client.release();
|
|
441
|
-
|
|
442
|
-
for (const work of store.afterCommit) {
|
|
443
|
-
try {
|
|
444
|
-
await work();
|
|
445
|
-
} catch (error) {
|
|
446
|
-
logger.error(`[db] after-commit work failed: ${error?.message ?? error}`);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
return out;
|
|
450
|
-
};
|
|
451
|
-
|
|
452
|
-
/* Work that must not happen before the data it announces is visible (a broadcast to clients): inside an open
|
|
453
|
-
transaction it waits for the COMMIT — and is dropped on a rollback; outside one it runs at once. */
|
|
454
|
-
export const afterCommit = (work) => {
|
|
455
|
-
const store = ambientTransactions.getStore();
|
|
456
|
-
if (store && !store.closed) {
|
|
457
|
-
store.afterCommit.push(work);
|
|
458
|
-
return undefined;
|
|
459
|
-
}
|
|
460
|
-
return work();
|
|
461
|
-
};
|
|
@@ -15,25 +15,8 @@
|
|
|
15
15
|
* and the publish reads it generically. No domain name, no domain code, nothing app-specific here.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
/*
|
|
21
|
-
* The route shape the client's menu component renders, identical to a hand-declared leaf.
|
|
22
|
-
*
|
|
23
|
-
* Doc 8 M3 — by the entry point's TYPE (`entryPointTypes`, from the domain's structures): a list opens as a list.
|
|
24
|
-
* Every leaf used to be a form link, so a list entry point opened as a form stub. An entry point of unknown type
|
|
25
|
-
* stays a form link, as before.
|
|
26
|
-
*/
|
|
27
|
-
const linkFor = (entry, entryPointTypes) => {
|
|
28
|
-
const entryPoint = String(entry?.entryPoint ?? "").trim();
|
|
29
|
-
return linkForType(entryPointTypes?.[entryPoint] === "list" ? "list" : "form", entryPoint);
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
/* A group path: `["FINA", "Master"]`, or a string for the one-level group it always was. */
|
|
33
|
-
const groupPath = (group) =>
|
|
34
|
-
(Array.isArray(group) ? group : [group]).map((caption) => String(caption ?? "").trim()).filter((caption) => caption !== "");
|
|
35
|
-
|
|
36
|
-
const isGroup = (node) => Array.isArray(node?.subMenu);
|
|
18
|
+
/* The route shape the client's menu component renders, identical to a hand-declared leaf. */
|
|
19
|
+
const linkFor = (entry) => ["./form", String(entry?.entryPoint ?? "").trim(), null];
|
|
37
20
|
|
|
38
21
|
/* The menu layouts the client shell can draw. */
|
|
39
22
|
const MENU_LAYOUTS = ["horizontal", "vertical"];
|
|
@@ -84,89 +67,70 @@ export const selectActiveDomainMenus = ({ menus, activeModules } = {}) => {
|
|
|
84
67
|
* something while the tile still opened. Injecting without stripping would also accumulate duplicates
|
|
85
68
|
* that nothing could clear.
|
|
86
69
|
*/
|
|
87
|
-
export const mergeDomainNavigation = ({ applicationConfig, domainKey, menus
|
|
70
|
+
export const mergeDomainNavigation = ({ applicationConfig, domainKey, menus } = {}) => {
|
|
88
71
|
const key = String(domainKey ?? "").trim();
|
|
89
72
|
const navigation = applicationConfig?.navigation ?? {};
|
|
90
73
|
const sourceMenus = Array.isArray(navigation.menus) ? navigation.menus : [];
|
|
91
74
|
const entries = Array.isArray(menus) ? menus : [];
|
|
92
75
|
|
|
93
|
-
/* Everything that is NOT this domain's,
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
76
|
+
/* Everything that is NOT this domain's, group structure preserved. */
|
|
77
|
+
const kept = sourceMenus.map((group) => ({
|
|
78
|
+
...group,
|
|
79
|
+
subMenu: (Array.isArray(group?.subMenu) ? group.subMenu : []).filter(
|
|
80
|
+
(leaf) => String(leaf?.domain ?? "") !== key,
|
|
81
|
+
),
|
|
82
|
+
}));
|
|
99
83
|
|
|
100
84
|
for (const entry of entries) {
|
|
101
|
-
const
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
let
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
level = group.subMenu;
|
|
130
|
-
});
|
|
85
|
+
const groupCaption = String(entry?.group ?? "").trim();
|
|
86
|
+
if (!groupCaption || !entry?.entryPoint) continue;
|
|
87
|
+
|
|
88
|
+
let group = kept.find((g) => String(g?.caption ?? "") === groupCaption);
|
|
89
|
+
if (!group) {
|
|
90
|
+
group = { caption: groupCaption, link: [], subMenu: [] };
|
|
91
|
+
/*
|
|
92
|
+
* ⚠️ WHERE a group the host does not have is placed. Appending is fine for one stray leaf
|
|
93
|
+
* and wrong for a domain that owns a whole group: "HR, beside Setup" appended after every
|
|
94
|
+
* group the host emits is a group at the BOTTOM of the menu, which is not what was asked
|
|
95
|
+
* for and not where anyone looks.
|
|
96
|
+
*
|
|
97
|
+
* ⚠️ ON CREATION ONLY. Once the group exists, `kept.find` above keeps its position, and so
|
|
98
|
+
* does the host's own injector, which concats into a group it finds rather than
|
|
99
|
+
* re-appending. Every entry of a multi-screen domain therefore carries the same hint and
|
|
100
|
+
* only the first one acts on it.
|
|
101
|
+
*
|
|
102
|
+
* ⚠️ AN ANCHOR THE HOST DOES NOT HAVE APPENDS. Declining to place the group would hide
|
|
103
|
+
* every screen the domain owns — far worse than placing it last, and a domain cannot know
|
|
104
|
+
* what groups its host happens to emit.
|
|
105
|
+
*/
|
|
106
|
+
const anchor = String(entry?.groupAfter ?? "").trim();
|
|
107
|
+
const at = anchor ? kept.findIndex((g) => String(g?.caption ?? "") === anchor) : -1;
|
|
108
|
+
if (at >= 0) kept.splice(at + 1, 0, group);
|
|
109
|
+
else kept.push(group);
|
|
110
|
+
}
|
|
111
|
+
if (!Array.isArray(group.subMenu)) group.subMenu = [];
|
|
131
112
|
|
|
132
|
-
|
|
113
|
+
group.subMenu.push({
|
|
133
114
|
caption: String(entry.caption ?? entry.entryPoint),
|
|
134
115
|
menuKey: String(entry.menuKey ?? entry.entryPoint),
|
|
135
|
-
link: linkFor(entry
|
|
116
|
+
link: linkFor(entry),
|
|
136
117
|
/* §3.3's stamp: what the next publish uses to find exactly its own leaves. */
|
|
137
118
|
domain: key,
|
|
138
119
|
});
|
|
139
120
|
}
|
|
140
121
|
|
|
141
|
-
/*
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
for (const node of nodes) {
|
|
152
|
-
if (!isGroup(node)) continue;
|
|
153
|
-
const path = [...prefix, String(node?.caption ?? "")];
|
|
154
|
-
if (foreignLeafUnder(node)) keepPaths.add(path.join("\u0000"));
|
|
155
|
-
collectKept(node.subMenu, path);
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
collectKept(sourceMenus, []);
|
|
159
|
-
const prune = (nodes, prefix) =>
|
|
160
|
-
nodes
|
|
161
|
-
.map((node) => {
|
|
162
|
-
if (!isGroup(node)) return node;
|
|
163
|
-
const path = [...prefix, String(node?.caption ?? "")];
|
|
164
|
-
return { ...node, subMenu: prune(node.subMenu, path), __path: path.join("\u0000") };
|
|
165
|
-
})
|
|
166
|
-
.filter((node) => !isGroup(node) || node.subMenu.length > 0 || keepPaths.has(node.__path))
|
|
167
|
-
.map(({ __path, ...node }) => node);
|
|
122
|
+
/* A group that existed only to hold this domain's leaves, and now holds none, goes with them —
|
|
123
|
+
an empty group renders as a dead parent nobody can open. The host's own empty groups are left
|
|
124
|
+
alone, because they were not this publish's to remove. */
|
|
125
|
+
const hadDomainLeafOnly = (group) =>
|
|
126
|
+
(group?.subMenu ?? []).length === 0 &&
|
|
127
|
+
!sourceMenus.some(
|
|
128
|
+
(original) =>
|
|
129
|
+
String(original?.caption ?? "") === String(group?.caption ?? "") &&
|
|
130
|
+
(original?.subMenu ?? []).some((leaf) => String(leaf?.domain ?? "") !== key),
|
|
131
|
+
);
|
|
168
132
|
|
|
169
|
-
return { ...navigation, menus:
|
|
133
|
+
return { ...navigation, menus: kept.filter((group) => !hadDomainLeafOnly(group)) };
|
|
170
134
|
};
|
|
171
135
|
|
|
172
136
|
/*
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* applicationConfig.js performs at publish time, so the runtime reads exactly what was published.
|
|
13
13
|
*/
|
|
14
14
|
import {
|
|
15
|
-
|
|
15
|
+
buildPostgresApplicationConfigLookupSql,
|
|
16
16
|
parseDomainConfig,
|
|
17
17
|
} from "./appConfig.js";
|
|
18
18
|
import { parseConfigData } from "./applicationConfig.js";
|
|
@@ -67,22 +67,10 @@ const LATEST_BY_NAME = (name) =>
|
|
|
67
67
|
`SELECT version, data FROM ${PG_CONFIG_TABLE} WHERE name = '${String(name).replace(/'/g, "''")}' ORDER BY id DESC LIMIT 1`;
|
|
68
68
|
|
|
69
69
|
/* Returns PARSED domain configs. `entityConfig.primeEntityConfig` takes this as its
|
|
70
|
-
`loadDomainConfigs`, which keeps that module a pure cache with no config-access dependency.
|
|
71
|
-
|
|
72
|
-
Doc 8 (2026-10-10): the domains of EVERY application on the tenant — the host's APPLICATION_CONFIG and
|
|
73
|
-
each `APPLICATION_CONFIG.<app>` — each domain once. A second app's entities are this hub's ORM
|
|
74
|
-
declarations and this tenant's tenancy just the same. */
|
|
70
|
+
`loadDomainConfigs`, which keeps that module a pure cache with no config-access dependency. */
|
|
75
71
|
export const createDomainConfigLoader = ({ exec, dbIndex }) => async () => {
|
|
76
|
-
const appRows = await exec(
|
|
77
|
-
const
|
|
78
|
-
const sources = [];
|
|
79
|
-
for (const appRow of appRows ?? []) {
|
|
80
|
-
for (const [name, version] of domainSourcesOf(appRow?.data)) {
|
|
81
|
-
if (seen.has(name)) continue;
|
|
82
|
-
seen.add(name);
|
|
83
|
-
sources.push([name, version]);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
72
|
+
const appRows = await exec(buildPostgresApplicationConfigLookupSql(), dbIndex);
|
|
73
|
+
const sources = domainSourcesOf(appRows?.[0]?.data);
|
|
86
74
|
|
|
87
75
|
const parsed = [];
|
|
88
76
|
for (const [name, namedVersion] of sources) {
|
|
@@ -42,8 +42,6 @@ export const findImplicitNumericScale = (config) => {
|
|
|
42
42
|
for (const [entityName, entity] of entitiesOf(config)) {
|
|
43
43
|
const attributes = entity?.attributes;
|
|
44
44
|
if (!attributes || typeof attributes !== "object" || Array.isArray(attributes)) continue;
|
|
45
|
-
/* Doc 8 M3: an external entity is another system's table; the publish creates none of its columns. */
|
|
46
|
-
if (entity?.external === true || String(entity?.external ?? "").trim().toLowerCase() === "true") continue;
|
|
47
45
|
/* An entity may name no table; the entity key is what a reader would search for. */
|
|
48
46
|
const table = String(entity?.table ?? entityName);
|
|
49
47
|
|
package/engine/domain/system.js
CHANGED
|
@@ -56,8 +56,6 @@ export const publishApp = async (data, argv = {}) => {
|
|
|
56
56
|
data?.domainId,
|
|
57
57
|
files: params.files || params.fileList,
|
|
58
58
|
body: publishPayload,
|
|
59
|
-
/* Doc 8 (2026-10-10): the SYS$APPS row the published config names (publishApplicationConfig). */
|
|
60
|
-
...(params.appMeta ? { appMeta: params.appMeta } : {}),
|
|
61
59
|
/* Bundle mode fetches the encryption key from the Biz-A server BEFORE uploading, so a
|
|
62
60
|
publish that loses the mode dials production even when the caller asked for local. */
|
|
63
61
|
serverMode: argv.serverMode || params.serverMode || "prod",
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
import { APPLICATION_THEME_NAME } from "../domain/themeConfig.js";
|
|
24
24
|
import { resolveThemeTokens } from "../domain/themePresets.js";
|
|
25
25
|
import { COMPANY_THEME_NAME } from "../orm/companyTheme.js";
|
|
26
|
-
import { COMPANY_LOCALE_NAME, readCompanyLocale } from "../orm/companyLocale.js";
|
|
27
26
|
import {
|
|
28
27
|
COMPANY_LOGO_NAME,
|
|
29
28
|
readCompanyLogo,
|
|
@@ -39,9 +38,6 @@ export const PUBLIC_THEME_KEYS = [
|
|
|
39
38
|
APPLICATION_THEME_NAME,
|
|
40
39
|
COMPANY_THEME_NAME,
|
|
41
40
|
COMPANY_LOGO_NAME,
|
|
42
|
-
/* Agung 2026-10-10 — the number format (id / en-US): display only, so a customer page shows the merchant's amounts
|
|
43
|
-
the merchant's way. */
|
|
44
|
-
COMPANY_LOCALE_NAME,
|
|
45
41
|
];
|
|
46
42
|
|
|
47
43
|
/*
|
|
@@ -122,7 +118,7 @@ export const createPublicThemeReader = ({ resolveTenant, execFor }) => {
|
|
|
122
118
|
}
|
|
123
119
|
};
|
|
124
120
|
|
|
125
|
-
const [applicationRow, companyRow, logoRow
|
|
121
|
+
const [applicationRow, companyRow, logoRow] = await Promise.all([
|
|
126
122
|
readOne(APPLICATION_THEME_NAME, "ID, DATA"),
|
|
127
123
|
readOne(COMPANY_THEME_NAME, "ID, DATA"),
|
|
128
124
|
/*
|
|
@@ -132,7 +128,6 @@ export const createPublicThemeReader = ({ resolveTenant, execFor }) => {
|
|
|
132
128
|
* time the theme is read. The image has its own endpoint and its own cache lifetime.
|
|
133
129
|
*/
|
|
134
130
|
readOne(COMPANY_LOGO_NAME, "ID"),
|
|
135
|
-
readOne(COMPANY_LOCALE_NAME, "ID, DATA"),
|
|
136
131
|
]);
|
|
137
132
|
|
|
138
133
|
const application = forCustomer(
|
|
@@ -149,8 +144,6 @@ export const createPublicThemeReader = ({ resolveTenant, execFor }) => {
|
|
|
149
144
|
three things a customer is allowed to see. */
|
|
150
145
|
name: company?.name ?? application?.name ?? null,
|
|
151
146
|
hasLogo: logoRow !== null,
|
|
152
|
-
/* the merchant's number format; a missing or bad row is Indonesian (readCompanyLocale's rule) */
|
|
153
|
-
numberLocale: (await readCompanyLocale({ exec: async () => (localeRow ? [localeRow] : []) })).number,
|
|
154
147
|
};
|
|
155
148
|
};
|
|
156
149
|
};
|