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
|
@@ -33,8 +33,7 @@ export const PRESENTATION = {
|
|
|
33
33
|
INDEX: [],
|
|
34
34
|
RELATIONSHIP: ["uiIntent", "label", "description"],
|
|
35
35
|
TENANCY: [],
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
package/engine/cc/publish.js
CHANGED
|
@@ -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
|
-
*
|
|
18
|
-
*
|
|
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
|
-
|
|
40
|
-
|
|
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
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
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
|
|