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.
Files changed (80) hide show
  1. package/bin/app.js +20 -255
  2. package/bin/hub.js +0 -16
  3. package/bin/hubEvent.js +5 -98
  4. package/bin/tunnelTarget.js +0 -4
  5. package/engine/bpm/workflow.js +6 -7
  6. package/engine/cc/configValidate.js +0 -53
  7. package/engine/cc/guides/V2-SCREEN-AUTHORING.md +512 -531
  8. package/engine/cc/layer3Behavior.js +1 -2
  9. package/engine/cc/layer3Schema.js +1 -3
  10. package/engine/cc/livePipelines.js +3 -9
  11. package/engine/cc/publish.js +105 -86
  12. package/engine/domain/appConfig.js +18 -108
  13. package/engine/domain/applicationConfig.js +12 -87
  14. package/engine/domain/dialect.js +0 -138
  15. package/engine/domain/domainNavigation.js +50 -86
  16. package/engine/domain/entityConfigLoader.js +4 -16
  17. package/engine/domain/numericScale.js +0 -2
  18. package/engine/domain/system.js +0 -2
  19. package/engine/ext/publicTheme.js +1 -8
  20. package/engine/orm/apiRoute.js +61 -187
  21. package/engine/orm/entityConfig.js +2 -32
  22. package/engine/orm/finaAuth.js +0 -0
  23. package/engine/orm/finaEndpoint.js +0 -19
  24. package/engine/orm/imgQuery.js +0 -92
  25. package/engine/orm/listQuery.js +1 -6
  26. package/package.json +1 -1
  27. package/worker/cliScriptWorker.js +0 -5
  28. package/engine/attachments/store.js +0 -195
  29. package/engine/domain/domainRegistration.js +0 -65
  30. package/engine/fina/approval.js +0 -141
  31. package/engine/fina/approvalService.js +0 -59
  32. package/engine/fina/attachments.js +0 -165
  33. package/engine/fina/client.js +0 -84
  34. package/engine/fina/config.js +0 -26
  35. package/engine/fina/context.js +0 -58
  36. package/engine/fina/document.js +0 -67
  37. package/engine/fina/documentWrite.js +0 -173
  38. package/engine/fina/documents/index.js +0 -28
  39. package/engine/fina/documents/jv.js +0 -105
  40. package/engine/fina/documents/jvWrite.js +0 -260
  41. package/engine/fina/documents/si.js +0 -40
  42. package/engine/fina/errors.js +0 -17
  43. package/engine/fina/features.js +0 -86
  44. package/engine/fina/guard.js +0 -102
  45. package/engine/fina/identity.js +0 -87
  46. package/engine/fina/live.js +0 -19
  47. package/engine/fina/lock.js +0 -35
  48. package/engine/fina/master.js +0 -331
  49. package/engine/fina/masters/coa.js +0 -104
  50. package/engine/fina/masters/currency.js +0 -39
  51. package/engine/fina/masters/department.js +0 -32
  52. package/engine/fina/masters/index.js +0 -21
  53. package/engine/fina/masters/project.js +0 -53
  54. package/engine/fina/period.js +0 -56
  55. package/engine/fina/print.js +0 -136
  56. package/engine/fina/readPolicy.js +0 -67
  57. package/engine/fina/recurring.js +0 -200
  58. package/engine/fina/route.js +0 -61
  59. package/engine/fina/routes/attachment.js +0 -41
  60. package/engine/fina/routes/document.js +0 -66
  61. package/engine/fina/routes/master.js +0 -26
  62. package/engine/fina/routes/period.js +0 -24
  63. package/engine/fina/routes/print.js +0 -19
  64. package/engine/fina/routes/recurring.js +0 -27
  65. package/engine/fina/routes/session.js +0 -23
  66. package/engine/fina/routes/setup.js +0 -58
  67. package/engine/fina/routes/submission.js +0 -46
  68. package/engine/fina/routes/template.js +0 -31
  69. package/engine/fina/settings.js +0 -159
  70. package/engine/fina/sql.js +0 -34
  71. package/engine/fina/submission.js +0 -290
  72. package/engine/fina/submissionCompare.js +0 -171
  73. package/engine/fina/submissionRepo.js +0 -114
  74. package/engine/fina/template.js +0 -118
  75. package/engine/fina/userMap.js +0 -79
  76. package/engine/orm/companyLocale.js +0 -74
  77. package/engine/orm/userApiDataset.js +0 -153
  78. package/migrations/postgres/1790600000000__fina.sql +0 -33
  79. package/migrations/postgres/1790700000000__attachment.sql +0 -29
  80. package/migrations/postgres/1790800000000__fina_jv_submission.sql +0 -41
@@ -33,8 +33,7 @@ export const PRESENTATION = {
33
33
  INDEX: [],
34
34
  RELATIONSHIP: ["uiIntent", "label", "description"],
35
35
  TENANCY: [],
36
- /* A list's search box and print button change how it is read, not what may be done with its records. */
37
- STRUCTURE: ["caption", "styleRef", "captionOK", "captionCancel", "width", "height", "label", "description", "search", "print"],
36
+ STRUCTURE: ["caption", "styleRef", "captionOK", "captionCancel", "width", "height", "label", "description"],
38
37
  ITEM: ["label", "title", "hidden", "styleRef", "className", "template", "icon", "variant"],
39
38
  ENTRY_POINT: ["version", "caption", "label", "description"],
40
39
  BINDING: [],
@@ -123,9 +123,7 @@ export const STYLE_PROFILE = [f("className", "text"), f("style", "keyValue"), f(
123
123
  const STRUCTURE_COMMON = [f("type", "enum", ["form", "list", "dialog"]), f("entity", "ref"), f("caption", "text"), f("styleRef", "ref")];
124
124
  export const STRUCTURE = {
125
125
  form: STRUCTURE_COMMON,
126
- /* Doc 8 M2: `search` (true, or { columns, placeholder }) and `print` (true, or { title }) are the list engine's own
127
- features. `selection` and `dbIndex` are Layer 2 only: mass actions call application handlers. */
128
- list: [...STRUCTURE_COMMON, f("filter", "json"), f("distinct", "boolean"), f("buttons", "json"), f("search", "json"), f("print", "json")],
126
+ list: [...STRUCTURE_COMMON, f("filter", "json"), f("distinct", "boolean"), f("buttons", "json")],
129
127
  dialog: [...STRUCTURE_COMMON, f("captionOK", "text"), f("captionCancel", "text"), f("width", "text"), f("height", "text")],
130
128
  };
131
129
 
@@ -8,17 +8,11 @@
8
8
  * workflow the engine's save (DRAFT) → publish (it validates again and assigns the version)
9
9
  *
10
10
  * Built per call for the verified caller (userId is the principal's, never the request's).
11
- *
12
- * Atomic publish (Agung 2026-10-08): `transaction` runs the whole publish in ONE transaction on the tenant database
13
- * (dialect.withPostgresTransaction — every call inside joins it). Trusted, as `biza add --publish` is: the DDL must not
14
- * run as biza_app on a tenant that declares tenancy. The principal gate (ccGate) has already run by then.
15
11
  */
16
12
  import fs from "node:fs";
17
13
  import os from "node:os";
18
14
  import path from "node:path";
19
- import { buildDomainDdlPreview, publishConfigs, resolveConfigFromPayload, saveConfigRow } from "../domain/appConfig.js";
20
- import { withPostgresTransaction } from "../domain/dialect.js";
21
- import { runTrusted } from "../orm/tenantContext.js";
15
+ import { buildDomainDdlPreview, deleteConfigRow, publishConfigs, resolveConfigFromPayload, saveConfigRow } from "../domain/appConfig.js";
22
16
  import { publishApplicationConfig } from "../domain/applicationConfig.js";
23
17
  import { validateTenancyDeclaration } from "../orm/tenancyDeclaration.js";
24
18
  import * as workflow from "../bpm/workflow.js";
@@ -33,9 +27,9 @@ export const createLivePipelines = ({ argv, userId }) => {
33
27
  return {
34
28
  ruleMode: ruleModeOf(argv),
35
29
 
36
- transaction: (fn) => runTrusted(() => withPostgresTransaction(dbIndex, fn)),
37
-
38
30
  saveConfig: (name, version, text) => saveConfigRow(name, version, text, config),
31
+ /* A domain whose DDL failed takes back the row it saved (publish.js). */
32
+ removeConfig: (name, version) => deleteConfigRow(name, version, config),
39
33
 
40
34
  publishDomain: (name, version) => publishConfigs({ domainName: name, domainVersion: version, dbIndex, migrationMode: "safe" }, argv),
41
35
 
@@ -14,10 +14,8 @@
14
14
  * 6. workflows — engine `save` (DRAFT) → `publish` (the engine assigns the version);
15
15
  * each draft is marked PUBLISHED as it lands.
16
16
  *
17
- * Atomic (Agung 2026-10-08): steps 4–6 run inside ONE transaction (`pipelines.transaction`). The first part that
18
- * fails ends it, and the whole publish is rolled back — rows, DDL, SYS$APPS, workflows, draft marks — and REPORTED
19
- * as `{ published: [], failed, rolledBack: true }`. (Found live 2026-10-07: a failed DDL left marketing@0.1.13
20
- * recorded, and a failed FK left the ALTERs before it.)
17
+ * ⚠️ The pipelines are not one transaction. A failure stops at that part and is REPORTED with what was
18
+ * already published; the drafts not reached stay OPEN, and publishing them again skips what is unchanged.
21
19
  *
22
20
  * Everything that writes is injected (`pipelines`), so this module decides and orders, and never talks
23
21
  * to a database itself.
@@ -26,7 +24,6 @@ import { validateWorkflow } from "../bpm/validateWorkflow.js";
26
24
  import { APPLICATION_NAME, layerOf, sha256, tryParse } from "./published.js";
27
25
  import { domainIndexOf, menuProblems } from "./appMenus.js";
28
26
  import { SNAKE_CASE, toSnakeCase } from "./layer3Schema.js";
29
- import { bumpPatch, registerDomainVersions, sourceName } from "../domain/domainRegistration.js";
30
27
 
31
28
  export const CC_PUBLISH_STALE = "CC_PUBLISH_STALE";
32
29
  export const CC_PUBLISH_INVALID = "CC_PUBLISH_INVALID";
@@ -36,8 +33,10 @@ const SEMVER = /^(\d+)\.(\d+)\.(\d+)$/;
36
33
  const isObj = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
37
34
  const refuse = (problems, code) => ({ ok: false, problems, ...(code ? { code } : {}) });
38
35
 
39
- /* The registration rule is shared with `biza add --register` (engine/domain/domainRegistration.js). */
40
- export { bumpPatch };
36
+ export const bumpPatch = (version) => {
37
+ const m = SEMVER.exec(String(version ?? ""));
38
+ return m ? `${m[1]}.${m[2]}.${Number(m[3]) + 1}` : "0.1.0";
39
+ };
41
40
 
42
41
  const newer = (a, b) => {
43
42
  const pa = SEMVER.exec(String(a ?? ""));
@@ -54,6 +53,8 @@ const withoutEngineKeys = (wf) => {
54
53
  return rest;
55
54
  };
56
55
 
56
+ /* A domain's source "name@version" → name. */
57
+ const sourceName = (source) => String(source ?? "").split("@")[0];
57
58
 
58
59
  async function publishedOf(repo, draft) {
59
60
  if (draft.kind === "DOMAIN") return repo.publishedDomain(draft.name);
@@ -188,107 +189,125 @@ export async function publishDrafts({ repo, userId, ids, listing, pipelines }) {
188
189
  }
189
190
  if (problems.length) return refuse(problems, CC_PUBLISH_INVALID);
190
191
 
191
- /* ---------------- 4–6. publish, in order — one transaction ------------------------------------ */
192
- if (typeof pipelines.transaction !== "function") return refuse(["The publish needs a transaction to run in; these pipelines do not provide one."]);
192
+ /* ---------------- 4–6. publish, in order ------------------------------------------------- */
193
193
  const result = { published: [], skipped: [], failed: null };
194
- /* The part being written: a failure anywhere in it — the pipeline, or marking its draft — is that part's. */
195
- let part = null;
196
- const PartFailed = class extends Error {};
197
- const fail = (kind, name, error) => {
198
- throw Object.assign(new PartFailed(String(error?.message ?? error)), { part: { kind, name } });
199
- };
200
194
  const landed = (d, version, skipped = false) => {
201
195
  (skipped ? result.skipped : result.published).push({ kind: d.kind, name: d.name, version: String(version) });
202
196
  return repo.markPublished(d.id, String(version), userId);
203
197
  };
198
+ const fail = (kind, name, error) => {
199
+ result.failed = { kind, name, error: String(error?.message ?? error) };
200
+ return { ok: true, data: result };
201
+ };
204
202
 
205
- const writeAll = async () => {
206
- /* Domains. */
207
- const domainVersions = new Map();
208
- for (const d of domainDrafts) {
209
- part = { kind: "DOMAIN", name: d.name };
210
- const p = published.get(d.id);
211
- const current = p ? tryParse("DOMAIN", p.text) : null;
212
- if (current && same(current, d.content)) {
213
- domainVersions.set(d.name, p.version);
214
- await landed(d, p.version, true);
215
- continue;
203
+ /* Domains. */
204
+ const domainVersions = new Map();
205
+ for (const d of domainDrafts) {
206
+ const p = published.get(d.id);
207
+ const current = p ? tryParse("DOMAIN", p.text) : null;
208
+ if (current && same(current, d.content)) {
209
+ domainVersions.set(d.name, p.version);
210
+ await landed(d, p.version, true);
211
+ continue;
212
+ }
213
+ const { version, content } = plan.get(d.id);
214
+ /* Found live 2026-10-06: the DDL failed AFTER the row was saved, and the row stayed — publishing the fixed draft
215
+ again was then refused as "published by someone else". The row this attempt saved is removed again (the
216
+ version is new: the conflict check above refuses a draft whose published row moved). */
217
+ let saved = false;
218
+ const undo = async () => {
219
+ if (!saved) return;
220
+ try {
221
+ await pipelines.removeConfig?.(d.name, version);
222
+ } catch {
223
+ /* reported with the failure below; the draft stays OPEN either way */
216
224
  }
217
- const { version, content } = plan.get(d.id);
225
+ };
226
+ try {
218
227
  await pipelines.saveConfig(d.name, version, JSON.stringify(content));
228
+ saved = true;
219
229
  const r = await pipelines.publishDomain(d.name, version);
220
- /* `publishConfigs` reports a failure rather than throwing it: thrown here, it rolls the publish back. */
221
- if (!r || r.success !== true) fail("DOMAIN", d.name, r?.error ?? "the domain publish reported no success.");
222
- domainVersions.set(d.name, version);
223
- await landed(d, version);
230
+ if (!r || r.success !== true) {
231
+ await undo();
232
+ return fail("DOMAIN", d.name, r?.error ?? "the domain publish reported no success.");
233
+ }
234
+ } catch (error) {
235
+ await undo();
236
+ return fail("DOMAIN", d.name, error);
224
237
  }
238
+ domainVersions.set(d.name, version);
239
+ await landed(d, version);
240
+ }
225
241
 
226
- /* Found live 2026-10-06: domains were published WITHOUT the open application draft that held their menu. The
227
- registration below wrote a new app row, and that draft — current until then — became "published by someone
228
- else". It is carried along: the same registration, its base moved to the row just written. A draft that was
229
- already stale is left alone (that conflict is the implementor's). */
230
- const carryOpenAppDraft = async (version, text, previousText) => {
231
- const open = ((await repo.openDrafts?.()) ?? []).find((x) => x.kind === "APPLICATION");
232
- if (!open || !isObj(open.content) || !repo.rebaseDraft) return;
233
- if (previousText == null || open.baseHash !== sha256(previousText)) return;
234
- const content = JSON.parse(JSON.stringify(open.content));
235
- content.domains = isObj(content.domains) ? content.domains : {};
236
- for (const [name, v] of domainVersions) {
237
- const key = Object.keys(content.domains).find((k) => sourceName(content.domains[k]?.source) === name) ?? name;
238
- content.domains[key] = { ...(content.domains[key] ?? {}), source: `${name}@${v}` };
239
- }
240
- const rebased = await repo.rebaseDraft(open.id, open.revision, { content, baseVersion: version, baseHash: sha256(text) }, userId);
241
- if (rebased) (result.rebased ??= []).push({ kind: "APPLICATION", id: open.id, baseVersion: version });
242
- };
242
+ /* Found live 2026-10-06: domains were published WITHOUT the open application draft that held their menu. The
243
+ registration below wrote a new app row, and that draft — current until then — became "published by someone
244
+ else". It is carried along: the same registration, its base moved to the row just written. A draft that was
245
+ already stale is left alone (that conflict is the implementor's). */
246
+ const carryOpenAppDraft = async (version, text, previousText) => {
247
+ const open = ((await repo.openDrafts?.()) ?? []).find((x) => x.kind === "APPLICATION");
248
+ if (!open || !isObj(open.content) || !repo.rebaseDraft) return;
249
+ if (previousText == null || open.baseHash !== sha256(previousText)) return;
250
+ const content = JSON.parse(JSON.stringify(open.content));
251
+ content.domains = isObj(content.domains) ? content.domains : {};
252
+ for (const [name, v] of domainVersions) {
253
+ const key = Object.keys(content.domains).find((k) => sourceName(content.domains[k]?.source) === name) ?? name;
254
+ content.domains[key] = { ...(content.domains[key] ?? {}), source: `${name}@${v}` };
255
+ }
256
+ const rebased = await repo.rebaseDraft(open.id, open.revision, { content, baseVersion: version, baseHash: sha256(text) }, userId);
257
+ if (rebased) (result.rebased ??= []).push({ kind: "APPLICATION", id: open.id, baseVersion: version });
258
+ };
243
259
 
244
- /* The application config: its domain entries, then SYS$APPS. */
245
- part = { kind: "APPLICATION", name: APPLICATION_NAME };
246
- const appDraft = drafts.find((d) => d.kind === "APPLICATION");
247
- const appRow = appDraft ? published.get(appDraft.id) : await repo.publishedApplication();
248
- const appBase = appDraft ? appDraft.content : appRow ? tryParse("APPLICATION", appRow.text) : null;
249
- if (domainVersions.size > 0 || appDraft) {
250
- if (!isObj(appBase)) fail("APPLICATION", APPLICATION_NAME, "there is no application config to add the domains to.");
251
- const app = registerDomainVersions(appBase, domainVersions);
252
- const currentApp = appRow ? tryParse("APPLICATION", appRow.text) : null;
253
- if (!appDraft && currentApp && same(currentApp, app)) {
254
- /* Every domain entry already names its version: nothing to rewrite. */
255
- } else if (appDraft && currentApp && same(currentApp, app)) {
256
- await landed(appDraft, appRow.version, true);
257
- } else {
258
- const version = bumpPatch(appRow?.version ?? appDraft?.baseVersion);
259
- if (isObj(app.metadata) && app.metadata.version !== undefined) app.metadata.version = version;
260
+ /* The application config: its domain entries, then SYS$APPS. */
261
+ const appDraft = drafts.find((d) => d.kind === "APPLICATION");
262
+ const appRow = appDraft ? published.get(appDraft.id) : await repo.publishedApplication();
263
+ const appBase = appDraft ? appDraft.content : appRow ? tryParse("APPLICATION", appRow.text) : null;
264
+ if (domainVersions.size > 0 || appDraft) {
265
+ if (!isObj(appBase)) return fail("APPLICATION", APPLICATION_NAME, "there is no application config to add the domains to.");
266
+ const app = JSON.parse(JSON.stringify(appBase));
267
+ app.domains = isObj(app.domains) ? app.domains : {};
268
+ for (const [name, version] of domainVersions) {
269
+ const key = Object.keys(app.domains).find((k) => sourceName(app.domains[k]?.source) === name) ?? name;
270
+ app.domains[key] = { ...(app.domains[key] ?? {}), source: `${name}@${version}` };
271
+ }
272
+ const currentApp = appRow ? tryParse("APPLICATION", appRow.text) : null;
273
+ if (!appDraft && currentApp && same(currentApp, app)) {
274
+ /* Every domain entry already names its version: nothing to rewrite. */
275
+ } else if (appDraft && currentApp && same(currentApp, app)) {
276
+ await landed(appDraft, appRow.version, true);
277
+ } else {
278
+ const version = bumpPatch(appRow?.version ?? appDraft?.baseVersion);
279
+ if (isObj(app.metadata) && app.metadata.version !== undefined) app.metadata.version = version;
280
+ try {
260
281
  await pipelines.saveConfig(APPLICATION_NAME, version, JSON.stringify(app));
261
282
  await pipelines.publishApplication(version);
262
- if (appDraft) await landed(appDraft, version);
263
- else {
264
- result.published.push({ kind: "APPLICATION", name: APPLICATION_NAME, version });
265
- await carryOpenAppDraft(version, JSON.stringify(app), appRow?.text);
266
- }
283
+ } catch (error) {
284
+ return fail("APPLICATION", APPLICATION_NAME, error);
285
+ }
286
+ if (appDraft) await landed(appDraft, version);
287
+ else {
288
+ result.published.push({ kind: "APPLICATION", name: APPLICATION_NAME, version });
289
+ await carryOpenAppDraft(version, JSON.stringify(app), appRow?.text);
267
290
  }
268
291
  }
292
+ }
269
293
 
270
- /* Workflows, through the engine: it validates again and assigns the version. */
271
- for (const d of drafts.filter((x) => x.kind === "WORKFLOW")) {
272
- part = { kind: "WORKFLOW", name: d.name };
273
- const p = published.get(d.id);
274
- const current = p ? tryParse("WORKFLOW", p.text) : null;
275
- if (current && same(withoutEngineKeys(current), withoutEngineKeys(d.content))) {
276
- await landed(d, p.version, true);
277
- continue;
278
- }
279
- const json = { ...d.content, status: "DRAFT" };
294
+ /* Workflows, through the engine: it validates again and assigns the version. */
295
+ for (const d of drafts.filter((x) => x.kind === "WORKFLOW")) {
296
+ const p = published.get(d.id);
297
+ const current = p ? tryParse("WORKFLOW", p.text) : null;
298
+ if (current && same(withoutEngineKeys(current), withoutEngineKeys(d.content))) {
299
+ await landed(d, p.version, true);
300
+ continue;
301
+ }
302
+ const json = { ...d.content, status: "DRAFT" };
303
+ try {
280
304
  const id = await pipelines.saveWorkflow(json);
281
305
  await pipelines.publishWorkflow(id, json);
282
306
  const row = await repo.publishedWorkflow(d.name);
283
307
  await landed(d, row?.version ?? "?");
308
+ } catch (error) {
309
+ return fail("WORKFLOW", d.name, error);
284
310
  }
285
- };
286
-
287
- try {
288
- await pipelines.transaction(writeAll);
289
- } catch (error) {
290
- const failed = error instanceof PartFailed ? error.part : (part ?? { kind: "DOMAIN", name: "?" });
291
- return { ok: true, data: { published: [], skipped: [], failed: { ...failed, error: String(error?.message ?? error) }, rolledBack: true } };
292
311
  }
293
312
  return { ok: true, data: result };
294
313
  }
@@ -269,50 +269,11 @@ export const buildPostgresConfigUpsertSql = (name, version, content, appendOnUpd
269
269
  SYS$CONFIG row through FINA and picks the APPLICATION_CONFIG one; here it is a direct lookup. */
270
270
  export const buildPostgresApplicationConfigLookupSql = () =>
271
271
  [
272
- `SELECT name, version, data FROM ${PG_CONFIG_TABLE}`,
272
+ `SELECT name, data FROM ${PG_CONFIG_TABLE}`,
273
273
  `WHERE upper(name) = '${APP_CONFIG_NAME}'`,
274
274
  "ORDER BY id DESC LIMIT 1;",
275
275
  ].join("\n");
276
276
 
277
- /*
278
- * Doc 8 (Agung 2026-10-10) — MORE THAN ONE APPLICATION ON A TENANT.
279
- *
280
- * The host app is `APPLICATION_CONFIG`, as it always was. Another app declares itself with
281
- * `metadata.app` in its application.js and lives under `APPLICATION_CONFIG.<app>`; its SYS$APPS row is
282
- * `<app>`. The client already runs several SYS$APPS rows side by side.
283
- *
284
- * ⚠️ `<app>` is lower-case letters and digits only. The client splits a link on `-` to find the app
285
- * (`<app>-<form>`), and `.` would make the config name ambiguous.
286
- */
287
- const SECONDARY_APP_NAME = /^[a-z][a-z0-9]*$/;
288
-
289
- export const appConfigNameFor = (metadata) => {
290
- const app = metadata?.app;
291
- if (app == null || app === "") return APP_CONFIG_NAME;
292
- if (typeof app !== "string" || !SECONDARY_APP_NAME.test(app)) {
293
- throw new Error(
294
- `metadata.app "${app}" is not a valid application name: use lower-case letters and digits only (no spaces, '-' or '.').`,
295
- );
296
- }
297
- return `${APP_CONFIG_NAME}.${app}`;
298
- };
299
-
300
- /* `APPLICATION_CONFIG.finacloud` → `finacloud`; the host config, or any other row, → null. */
301
- export const secondaryAppNameOf = (configName) => {
302
- const match = /^APPLICATION_CONFIG\.([a-z0-9]+)$/i.exec(String(configName ?? "").trim());
303
- return match ? match[1].toLowerCase() : null;
304
- };
305
-
306
- /* The newest row of EVERY application config — the host's and each secondary app's — in one read.
307
- ⚠️ An exact pattern, not a prefix: the sibling rows `APPLICATION_CONFIG.<app>__UPLOAD_SOURCE` /
308
- `__PUBLISH_STATUS` share the prefix and are not application configs. */
309
- export const buildPostgresApplicationConfigsLookupSql = () =>
310
- [
311
- `SELECT DISTINCT ON (upper(name)) name, version, data FROM ${PG_CONFIG_TABLE}`,
312
- `WHERE upper(name) ~ '^${APP_CONFIG_NAME}(\\.[A-Z0-9]+)?$'`,
313
- "ORDER BY upper(name), id DESC;",
314
- ].join("\n");
315
-
316
277
  /* The client app bundle. On Firebird this goes through the FINA stored procedure
317
278
  TOrmMethod/"setApp"; SYS$APPS is a Biz-A table (created by migrations/postgres/…__apps.sql), so
318
279
  on PostgreSQL the CLI writes it directly. UPDATEAT is left to the BEFORE UPDATE trigger that
@@ -2014,20 +1975,6 @@ const applyDirectRelationship = (relationship, entityInfos, warnings, adjustment
2014
1975
  const fromForeignKey = resolveEntityColumnName(fromInfo, relationship.foreignKey);
2015
1976
 
2016
1977
  let ownerInfo = toForeignKey ? toInfo : fromForeignKey ? fromInfo : toInfo;
2017
- /*
2018
- * Doc 8 M3 — an EXTERNAL table is never altered, so it never owns the foreign key: when the other side can, it does
2019
- * (a Biz-A table looking up FINA's GLACCNT by GLACCOUNT, a column both declare). When neither side can, the
2020
- * relationship is the screens' business only and nothing is built for it.
2021
- */
2022
- if (ownerInfo.external) {
2023
- const other = ownerInfo === toInfo ? fromInfo : toInfo;
2024
- const otherHasKey = other === fromInfo ? fromForeignKey : toForeignKey;
2025
- if (other.external || !otherHasKey) {
2026
- adjustments.push(`Relationship "${relationship.from}->${relationship.to}": no column or foreign key is built on the external table ${ownerInfo.tableName}.`);
2027
- return;
2028
- }
2029
- ownerInfo = other;
2030
- }
2031
1978
  const foreignKey =
2032
1979
  ownerInfo === toInfo
2033
1980
  ? (toForeignKey ?? toDbName(relationship.foreignKey))
@@ -2075,9 +2022,7 @@ const applyDirectRelationship = (relationship, entityInfos, warnings, adjustment
2075
2022
  onDelete,
2076
2023
  onUpdate: "NO ACTION",
2077
2024
  };
2078
- /* Doc 8 M3 — the referenced table is another system's (often in another database): the column and its index are
2079
- Biz-A's, a constraint pointing at a table this database does not have is not. */
2080
- if (!referenceInfo.external) ensureForeignKey(ownerInfo.table, fk);
2025
+ ensureForeignKey(ownerInfo.table, fk);
2081
2026
 
2082
2027
  const relationIndexColumns = [foreignKey];
2083
2028
  if (
@@ -2182,27 +2127,22 @@ const applyManyToManyRelationship = (relationship, entityInfos, tableInfos, tabl
2182
2127
  }
2183
2128
 
2184
2129
  const onDelete = relationship.ownership === "parent" ? "CASCADE" : "NO ACTION";
2185
- /* Doc 8 M3 — no constraint points at an EXTERNAL side; the through table's columns and indexes stay. */
2186
- if (!fromInfo.external) {
2187
- ensureForeignKey(throughInfo.table, {
2188
- name: buildObjectName("FK", throughInfo.tableName, fromInfo.tableName, through.fromKey),
2189
- columns: [through.fromKey],
2190
- referenceTable: fromInfo.tableName,
2191
- referenceColumns: [fromPk],
2192
- onDelete,
2193
- onUpdate: "NO ACTION",
2194
- });
2195
- }
2196
- if (!toInfo.external) {
2197
- ensureForeignKey(throughInfo.table, {
2198
- name: buildObjectName("FK", throughInfo.tableName, toInfo.tableName, through.toKey),
2199
- columns: [through.toKey],
2200
- referenceTable: toInfo.tableName,
2201
- referenceColumns: [toPk],
2202
- onDelete,
2203
- onUpdate: "NO ACTION",
2204
- });
2205
- }
2130
+ ensureForeignKey(throughInfo.table, {
2131
+ name: buildObjectName("FK", throughInfo.tableName, fromInfo.tableName, through.fromKey),
2132
+ columns: [through.fromKey],
2133
+ referenceTable: fromInfo.tableName,
2134
+ referenceColumns: [fromPk],
2135
+ onDelete,
2136
+ onUpdate: "NO ACTION",
2137
+ });
2138
+ ensureForeignKey(throughInfo.table, {
2139
+ name: buildObjectName("FK", throughInfo.tableName, toInfo.tableName, through.toKey),
2140
+ columns: [through.toKey],
2141
+ referenceTable: toInfo.tableName,
2142
+ referenceColumns: [toPk],
2143
+ onDelete,
2144
+ onUpdate: "NO ACTION",
2145
+ });
2206
2146
 
2207
2147
  ensureIndex(throughInfo.table, {
2208
2148
  name: buildObjectName("IDX", throughInfo.tableName, through.fromKey),
@@ -2261,26 +2201,6 @@ const applyServerValueFacts = (entities, relationships, entityInfos, warnings) =
2261
2201
  }
2262
2202
  };
2263
2203
 
2264
- /* Doc 8 M3 — `external: true`: Biz-A reads the table and never creates, alters or links to it. */
2265
- export const isExternalEntity = (entity) => entity?.external === true || String(entity?.external ?? "").trim().toLowerCase() === "true";
2266
-
2267
- /*
2268
- * What only a Biz-A table can carry. Each is a model WARNING — the publish stops, as for any declaration the database
2269
- * cannot keep — never silently dropped: a `numbering` or `follows` is a trigger Biz-A would have to install on the
2270
- * other system's table, and tenancy is a column and a policy it would have to add.
2271
- */
2272
- const externalEntityProblems = (entityName, entity, tableName, tenantTable) => {
2273
- const problems = [];
2274
- const at = `${entityName} is external (another system's table, which Biz-A never alters)`;
2275
- for (const [attributeName, attribute] of Object.entries(entity?.attributes ?? {})) {
2276
- if (attribute?.numbering !== undefined) problems.push(`${at}: ${attributeName} cannot declare numbering.`);
2277
- if (attribute?.follows !== undefined) problems.push(`${at}: ${attributeName} cannot declare follows.`);
2278
- }
2279
- if (entity?.tenanted || entity?.multiTenant) problems.push(`${at}: it cannot be tenanted.`);
2280
- if (tenantTable && String(tableName ?? "").trim().toLowerCase() === tenantTable) problems.push(`${at}: it cannot be the tenancy entity.`);
2281
- return problems;
2282
- };
2283
-
2284
2204
  const buildDomainSchemaInternal = (domainConfigContent, defaultDbIndex, tenancy = null) => {
2285
2205
  const parsed = parseDomainConfig(domainConfigContent);
2286
2206
  const model = parsed?.model ?? {};
@@ -2305,16 +2225,6 @@ const buildDomainSchemaInternal = (domainConfigContent, defaultDbIndex, tenancy
2305
2225
  const runtime = buildEntityTable(entityName, entity, defaultDbIndex, tenantTable);
2306
2226
  entityInfos.set(entityName, runtime);
2307
2227
  tableInfos.set(runtime.tableName, runtime);
2308
- /*
2309
- * Doc 8 M3 — an EXTERNAL entity is a table another system owns (FINA's GLACCNT…): declared for the screens,
2310
- * never created, altered or linked to. Its runtime info stays, so relationships can still resolve its columns;
2311
- * its table never reaches the schema the DDL sync runs.
2312
- */
2313
- if (isExternalEntity(entity)) {
2314
- runtime.external = true;
2315
- warnings.push(...externalEntityProblems(entityName, entity, runtime.tableName, tenantTable));
2316
- continue;
2317
- }
2318
2228
  tables.push(runtime.table);
2319
2229
  }
2320
2230