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
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Doc 8 M4 — `fina/recurring/*`: recurring detail lines, shared with FINA desktop (T-33–T-38, D8).
|
|
3
|
-
*
|
|
4
|
-
* `list { document }` and `get { document, id }`: any mapped FINA user (T-36). `save { document, name, rows }`: the
|
|
5
|
-
* create right; the name is new (T-34). `remove { document, id }`: the edit right (D4). There is no edit (T-37).
|
|
6
|
-
*
|
|
7
|
-
* ⚠️ Registered as an engine target: export routes only. `remove`, because `delete` is a reserved word.
|
|
8
|
-
*/
|
|
9
|
-
import { runFinaRoute } from "../route.js";
|
|
10
|
-
import { createIdentity } from "../identity.js";
|
|
11
|
-
import { createRecurringService } from "../recurring.js";
|
|
12
|
-
|
|
13
|
-
const service = (deps) => createRecurringService({ client: deps.client, identity: createIdentity(deps) });
|
|
14
|
-
|
|
15
|
-
export const list = (payload, _argv, context) =>
|
|
16
|
-
runFinaRoute({ name: "recurring/list", context }, ({ deps, principal }) => service(deps).list(principal, payload?.document));
|
|
17
|
-
|
|
18
|
-
export const get = (payload, _argv, context) =>
|
|
19
|
-
runFinaRoute({ name: "recurring/get", context }, ({ deps, principal }) => service(deps).get(principal, payload?.document, payload?.id));
|
|
20
|
-
|
|
21
|
-
export const save = (payload, _argv, context) =>
|
|
22
|
-
runFinaRoute({ name: "recurring/save", context }, ({ deps, principal }) =>
|
|
23
|
-
service(deps).save(principal, payload?.document, { name: payload?.name, rows: payload?.rows }),
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
export const remove = (payload, _argv, context) =>
|
|
27
|
-
runFinaRoute({ name: "recurring/remove", context }, ({ deps, principal }) => service(deps).remove(principal, payload?.document, payload?.id));
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Doc 8 — `fina/session/*`: for any signed-in user of a FINA Cloud company.
|
|
3
|
-
*
|
|
4
|
-
* `me` says who the caller is in FINA and what they may do (S-02), so screens can hide what is not allowed (S-04),
|
|
5
|
-
* plus the date lock, the FINA features and the module settings (M4: the JV's default template, rate editable,
|
|
6
|
-
* mandatory dimensions) the screens depend on. The server enforces all of it again on every
|
|
7
|
-
* write; this answer is for the UI only.
|
|
8
|
-
*
|
|
9
|
-
* ⚠️ Registered as an engine target: export routes only.
|
|
10
|
-
*/
|
|
11
|
-
import { runFinaRoute } from "../route.js";
|
|
12
|
-
import { createIdentity } from "../identity.js";
|
|
13
|
-
import { createSettingsStore } from "../settings.js";
|
|
14
|
-
import { createFeatures } from "../features.js";
|
|
15
|
-
|
|
16
|
-
export const me = (_payload, _argv, context) =>
|
|
17
|
-
runFinaRoute({ name: "session/me", context }, async ({ deps, principal, config }) => {
|
|
18
|
-
const identity = await createIdentity(deps).resolve(principal);
|
|
19
|
-
const settings = await createSettingsStore(deps).read();
|
|
20
|
-
const features = await createFeatures({ client: deps.client }).read();
|
|
21
|
-
/* Doc 8 M3: dbIndex = the FINA database the master lists read (the operator flag), so no config hard-codes it. */
|
|
22
|
-
return { ...identity, lockUpTo: settings.lock.upTo, features, dbIndex: config?.dbIndex ?? null, settings: { jv: settings.jv } };
|
|
23
|
-
});
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Doc 8 M1 — `fina/setup/*`: the server side of the FINA Cloud setup console (spec §3). ADMINISTRATORS only.
|
|
3
|
-
*
|
|
4
|
-
* ⚠️ Registered as an engine target (bin/hubEvent.js EngineRegistry), so EVERY export here is callable from a
|
|
5
|
-
* browser. Export routes only; the work lives in the modules beside this folder.
|
|
6
|
-
*/
|
|
7
|
-
import { runFinaRoute } from "../route.js";
|
|
8
|
-
import { createFeatures } from "../features.js";
|
|
9
|
-
import { createSettingsStore } from "../settings.js";
|
|
10
|
-
import { createUserMap } from "../userMap.js";
|
|
11
|
-
import { listTemplates, TEMPLATE_TYPE } from "../template.js";
|
|
12
|
-
import { publishApproval } from "../approval.js";
|
|
13
|
-
|
|
14
|
-
const admin = (name, context, work) => runFinaRoute({ name: `setup/${name}`, context, admin: true }, work);
|
|
15
|
-
|
|
16
|
-
/* Everything the console shows on load (C-04, C-05, §3.4, C-14). */
|
|
17
|
-
export const state = (_payload, _argv, context) =>
|
|
18
|
-
admin("state", context, async ({ deps, config }) => ({
|
|
19
|
-
finaDbIndex: config.dbIndex,
|
|
20
|
-
features: await createFeatures({ client: deps.client }).read(),
|
|
21
|
-
settings: await createSettingsStore(deps).read(),
|
|
22
|
-
templates: await listTemplates(deps.client, TEMPLATE_TYPE.JV),
|
|
23
|
-
}));
|
|
24
|
-
|
|
25
|
-
/* C-05: Multi Department / Multi Project, written to FINA itself. C-12 refuses switching one off while used. */
|
|
26
|
-
export const saveFeatures = (payload, _argv, context) =>
|
|
27
|
-
admin("saveFeatures", context, async ({ deps }) =>
|
|
28
|
-
createFeatures({ client: deps.client }).save({ multiDept: payload?.multiDept, multiProject: payload?.multiProject }),
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
/* §3.4 / C-14: `{ jv?: {...}, lock?: {...} }`, only the keys being changed. Answers every section as stored. */
|
|
32
|
-
export const saveSettings = (payload, _argv, context) =>
|
|
33
|
-
admin("saveSettings", context, async ({ deps, principal }) => createSettingsStore(deps).write(payload, principal.userId));
|
|
34
|
-
|
|
35
|
-
/* S-01: the Biz-A users, the active FINA users and the current map. */
|
|
36
|
-
export const users = (_payload, _argv, context) => admin("users", context, async ({ deps }) => createUserMap(deps).list());
|
|
37
|
-
|
|
38
|
-
/* S-01: `{ rows: [{ bizaUserId, finaUserId | null }] }`. */
|
|
39
|
-
export const saveUserMap = (payload, _argv, context) =>
|
|
40
|
-
admin("saveUserMap", context, async ({ deps, principal }) => createUserMap(deps).save(payload, principal.userId));
|
|
41
|
-
|
|
42
|
-
/*
|
|
43
|
-
* Doc 8 M7 (A-02, A-04): the console Publish with Need Approval on — the JV approval workflow and its two effect
|
|
44
|
-
* scripts, on this hub's database. A definition someone changed in the BPM module is kept (A-03); the same one is not
|
|
45
|
-
* published again.
|
|
46
|
-
*/
|
|
47
|
-
export const approval = (_payload, _argv, context) =>
|
|
48
|
-
admin("approval", context, async ({ deps, principal, config }) => {
|
|
49
|
-
const workflow = deps.workflow ?? (await import("../../bpm/workflow.js"));
|
|
50
|
-
const apiConfig = { dbIndex: config.tenantDbIndex };
|
|
51
|
-
return publishApproval({
|
|
52
|
-
exec: deps.exec,
|
|
53
|
-
execStatements: deps.execStatements,
|
|
54
|
-
save: (p) => workflow.save({ ...p, apiConfig }),
|
|
55
|
-
publish: (p) => workflow.publish({ ...p, apiConfig }),
|
|
56
|
-
userId: principal.userId,
|
|
57
|
-
});
|
|
58
|
-
});
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Doc 8 M7 — JV submissions (pengajuan), with Need Approval on (submission.js).
|
|
3
|
-
*
|
|
4
|
-
* - `fina/submission/get` `{ id }`: its JV (master, lines), number, status, the FINA JV it became, why posting failed,
|
|
5
|
-
* and — while it waits — what would stop its posting now (A-18). Any mapped FINA user.
|
|
6
|
-
* - `fina/submission/save` `{ id|null, master, lines, requestId }`: a new one (the create right) gets its number and
|
|
7
|
-
* its approval workflow; a waiting one is edited (the edit right, A-17). The JV rules, the Biz-A lock and FINA's
|
|
8
|
-
* period are checked as for a JV. Answers `{ id, number }`.
|
|
9
|
-
* - `fina/submission/remove` `{ id, requestId }`: a waiting one only (M-20) — its workflow ended, its files and the row
|
|
10
|
-
* gone.
|
|
11
|
-
* - `fina/submission/compare` `{ id }` (Doc 8 M7b, L-02 / L-09 / A-16): the submission next to the JV it became in
|
|
12
|
-
* FINA — both sides with FINA's display values, `jvState` POSTED / NOT_FOUND / NOT_POSTED, and the differences.
|
|
13
|
-
* Any mapped FINA user.
|
|
14
|
-
*
|
|
15
|
-
* Setujui / Tolak are the workflow's transitions (bpm/workflowRT, the approver role); their effects post or close it.
|
|
16
|
-
*
|
|
17
|
-
* ⚠️ Registered as an engine target: export routes only.
|
|
18
|
-
*/
|
|
19
|
-
import { runFinaRoute } from "../route.js";
|
|
20
|
-
import { submissionServiceOf } from "../approvalService.js";
|
|
21
|
-
import { compareSubmission } from "../submissionCompare.js";
|
|
22
|
-
import { createIdentity } from "../identity.js";
|
|
23
|
-
import { createSubmissionRepo } from "../submissionRepo.js";
|
|
24
|
-
|
|
25
|
-
export const get = (payload, _argv, context) =>
|
|
26
|
-
runFinaRoute({ name: "submission/get", context }, ({ deps, principal, config }) => submissionServiceOf(deps, config).get(principal, payload?.id));
|
|
27
|
-
|
|
28
|
-
export const save = (payload, _argv, context) =>
|
|
29
|
-
runFinaRoute({ name: "submission/save", context }, ({ deps, principal, config }) =>
|
|
30
|
-
submissionServiceOf(deps, config).save(
|
|
31
|
-
principal,
|
|
32
|
-
{ id: payload?.id, master: payload?.master, lines: payload?.lines, requestId: payload?.requestId },
|
|
33
|
-
{ dbIndex: config?.dbIndex },
|
|
34
|
-
),
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
/* `remove` because `delete` is a reserved word (as for documents). */
|
|
38
|
-
export const remove = (payload, _argv, context) =>
|
|
39
|
-
runFinaRoute({ name: "submission/remove", context }, ({ deps, principal, config }) =>
|
|
40
|
-
submissionServiceOf(deps, config).remove(principal, { id: payload?.id, requestId: payload?.requestId }, { dbIndex: config?.dbIndex }),
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
export const compare = (payload, _argv, context) =>
|
|
44
|
-
runFinaRoute({ name: "submission/compare", context }, ({ deps, principal }) =>
|
|
45
|
-
compareSubmission({ client: deps.client, identity: createIdentity(deps), repo: createSubmissionRepo({ exec: deps.exec }) }, principal, payload?.id),
|
|
46
|
-
);
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Doc 8 M4 — `fina/template/get`: what a document's form is laid out from (T-01–T-16, T-22, T-23).
|
|
3
|
-
*
|
|
4
|
-
* Payload `{ document, templateId? }`. Answers the type's templates (for the selector), the chosen one (asked → the
|
|
5
|
-
* console's default → FINA's default → the first), its elements, and the extended fields mapped onto the document's
|
|
6
|
-
* tables. Any mapped FINA user (D4: view).
|
|
7
|
-
*
|
|
8
|
-
* ⚠️ Registered as an engine target: export routes only.
|
|
9
|
-
*/
|
|
10
|
-
import { runFinaRoute } from "../route.js";
|
|
11
|
-
import { createIdentity, requireRight } from "../identity.js";
|
|
12
|
-
import { createSettingsStore } from "../settings.js";
|
|
13
|
-
import { documentOf } from "../documents/index.js";
|
|
14
|
-
import { listTemplates, chooseTemplateId, assertTemplateOfType, readTemplateElements, readMappings } from "../template.js";
|
|
15
|
-
|
|
16
|
-
export const get = (payload, _argv, context) =>
|
|
17
|
-
runFinaRoute({ name: "template/get", context }, async ({ deps, principal }) => {
|
|
18
|
-
const document = documentOf(payload?.document);
|
|
19
|
-
requireRight(await createIdentity(deps).resolve(principal), document.rights, null);
|
|
20
|
-
const templates = await listTemplates(deps.client, document.templateType);
|
|
21
|
-
const asked = payload?.templateId;
|
|
22
|
-
const requested = asked === null || asked === undefined || asked === "" ? null : assertTemplateOfType(templates, asked);
|
|
23
|
-
const settings = await createSettingsStore(deps).read();
|
|
24
|
-
const templateId = chooseTemplateId({ templates, requested, settingDefault: settings?.[document.name]?.defaultTemplateId });
|
|
25
|
-
return {
|
|
26
|
-
templates,
|
|
27
|
-
templateId,
|
|
28
|
-
elements: await readTemplateElements(deps.client, templateId),
|
|
29
|
-
mappings: await readMappings(deps.client, document.extended),
|
|
30
|
-
};
|
|
31
|
-
});
|
package/engine/fina/settings.js
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Doc 8 — the FINA Cloud settings the SERVER enforces, in engine/fina's own table FINA$SETTING (one JSON value per
|
|
3
|
-
* section). The setup console writes them through `fina/setup/saveSettings`; nothing else may.
|
|
4
|
-
*
|
|
5
|
-
* - `lock`: the Biz-A date lock (C-14, D2a).
|
|
6
|
-
* - `jv`: Need Approval (A-01), the default template (§3.4, T-22), rate editable (F-18), department / project
|
|
7
|
-
* mandatory (F-09, D5), attachment limits (F-35). Biz-A only, never written to FINA (C-13).
|
|
8
|
-
*
|
|
9
|
-
* ⚠️ CLOSED SCHEMA. An unknown section or key is refused, never stored: a misspelt setting that was silently kept
|
|
10
|
-
* would leave the implementor believing a rule was on.
|
|
11
|
-
*/
|
|
12
|
-
import { finaError } from "./errors.js";
|
|
13
|
-
import { toDateOnly } from "./lock.js";
|
|
14
|
-
import { logger } from "../../logger.js";
|
|
15
|
-
|
|
16
|
-
export const SETTING_DEFAULTS = Object.freeze({
|
|
17
|
-
lock: Object.freeze({ upTo: null }),
|
|
18
|
-
jv: Object.freeze({
|
|
19
|
-
needApproval: false,
|
|
20
|
-
/* null = FINA's own default template (TEMPLATE.ISDEFTEMPLATE). */
|
|
21
|
-
defaultTemplateId: null,
|
|
22
|
-
/* FINA desktop always lets the rate be edited; Biz-A matches it until the implementor says otherwise. */
|
|
23
|
-
rateEditable: true,
|
|
24
|
-
deptMandatory: false,
|
|
25
|
-
projectMandatory: false,
|
|
26
|
-
attachments: Object.freeze({ maxFiles: 5, maxFileMb: 5, extensions: Object.freeze(["pdf", "jpg", "jpeg", "png"]) }),
|
|
27
|
-
}),
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
const isPlain = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
31
|
-
const clone = (v) => JSON.parse(JSON.stringify(v));
|
|
32
|
-
|
|
33
|
-
/* One level of nesting is all the schema has (jv.attachments). */
|
|
34
|
-
const lay = (base, over) => {
|
|
35
|
-
const out = clone(base);
|
|
36
|
-
if (!isPlain(over)) return out;
|
|
37
|
-
for (const [key, value] of Object.entries(over)) {
|
|
38
|
-
out[key] = isPlain(out[key]) && isPlain(value) ? { ...out[key], ...value } : value;
|
|
39
|
-
}
|
|
40
|
-
return out;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const EXTENSION = /^[a-z0-9]{1,10}$/;
|
|
44
|
-
|
|
45
|
-
/* Each check answers null when the value is fine, or the reason. `normalise` turns an accepted value into what is
|
|
46
|
-
stored. */
|
|
47
|
-
const SCHEMA = {
|
|
48
|
-
lock: {
|
|
49
|
-
upTo: {
|
|
50
|
-
normalise: (v) => (v === null || v === "" ? null : toDateOnly(v)),
|
|
51
|
-
check: (v) => (v === null || v === "" || (typeof v === "string" && toDateOnly(v) === v) ? null : "must be a date (YYYY-MM-DD) or empty"),
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
jv: {
|
|
55
|
-
needApproval: { check: (v) => (typeof v === "boolean" ? null : "must be true or false") },
|
|
56
|
-
defaultTemplateId: {
|
|
57
|
-
normalise: (v) => (v === null || v === "" ? null : Number(v)),
|
|
58
|
-
check: (v) => (v === null || v === "" || (Number.isInteger(Number(v)) && Number(v) > 0 && !/\D/.test(String(v))) ? null : "must be a positive whole number or empty"),
|
|
59
|
-
},
|
|
60
|
-
rateEditable: { check: (v) => (typeof v === "boolean" ? null : "must be true or false") },
|
|
61
|
-
deptMandatory: { check: (v) => (typeof v === "boolean" ? null : "must be true or false") },
|
|
62
|
-
projectMandatory: { check: (v) => (typeof v === "boolean" ? null : "must be true or false") },
|
|
63
|
-
attachments: {
|
|
64
|
-
nested: {
|
|
65
|
-
maxFiles: { check: (v) => (Number.isInteger(v) && v >= 0 && v <= 100 ? null : "must be a whole number from 0 to 100") },
|
|
66
|
-
maxFileMb: { check: (v) => (typeof v === "number" && Number.isFinite(v) && v > 0 && v <= 100 ? null : "must be a number above 0 and at most 100") },
|
|
67
|
-
extensions: {
|
|
68
|
-
normalise: (v) => [...new Set(v.map((e) => String(e).trim().toLowerCase().replace(/^\./, "")))],
|
|
69
|
-
check: (v) =>
|
|
70
|
-
Array.isArray(v) && v.length <= 30 && v.every((e) => EXTENSION.test(String(e).trim().toLowerCase().replace(/^\./, "")))
|
|
71
|
-
? null
|
|
72
|
-
: "must be a list of up to 30 file extensions (letters and digits, at most 10 each)",
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
/*
|
|
80
|
-
* Validate a PATCH (only the sections and keys being changed) against the current settings. Answers
|
|
81
|
-
* `{ ok: true, value: { <section>: <the whole merged section> } }` for the sections the patch names, or
|
|
82
|
-
* `{ ok: false, problems: [...] }` listing every reason at once.
|
|
83
|
-
*/
|
|
84
|
-
export const validateSettings = (patch, current = SETTING_DEFAULTS) => {
|
|
85
|
-
const problems = [];
|
|
86
|
-
const value = {};
|
|
87
|
-
if (!isPlain(patch)) return { ok: false, problems: ["the settings must be an object of sections"] };
|
|
88
|
-
|
|
89
|
-
const walk = (schema, input, prefix, out) => {
|
|
90
|
-
if (!isPlain(input)) {
|
|
91
|
-
problems.push(`${prefix} must be an object`);
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
for (const [key, raw] of Object.entries(input)) {
|
|
95
|
-
const rule = schema[key];
|
|
96
|
-
const name = `${prefix}.${key}`;
|
|
97
|
-
if (!rule) {
|
|
98
|
-
problems.push(`${name} is not a setting`);
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
if (rule.nested) {
|
|
102
|
-
out[key] = isPlain(out[key]) ? out[key] : {};
|
|
103
|
-
walk(rule.nested, raw, name, out[key]);
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
const reason = rule.check(raw);
|
|
107
|
-
if (reason) problems.push(`${name} ${reason}`);
|
|
108
|
-
else out[key] = rule.normalise ? rule.normalise(raw) : raw;
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
for (const [section, input] of Object.entries(patch)) {
|
|
113
|
-
if (!SCHEMA[section]) {
|
|
114
|
-
problems.push(`unknown setting section "${section}"`);
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
const merged = lay(current?.[section] ?? SETTING_DEFAULTS[section], {});
|
|
118
|
-
walk(SCHEMA[section], input, section, merged);
|
|
119
|
-
value[section] = merged;
|
|
120
|
-
}
|
|
121
|
-
return problems.length > 0 ? { ok: false, problems } : { ok: true, value };
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
const quote = (s) => `'${String(s).replace(/'/g, "''")}'`;
|
|
125
|
-
const actorSql = (actorId) => (Number.isInteger(actorId) ? String(actorId) : /^\d+$/.test(String(actorId ?? "")) ? String(Number(actorId)) : "NULL");
|
|
126
|
-
|
|
127
|
-
export const createSettingsStore = ({ exec, execStatements }) => {
|
|
128
|
-
const read = async () => {
|
|
129
|
-
const rows = (await exec("SELECT KEY, VALUE FROM FINA$SETTING")) ?? [];
|
|
130
|
-
const stored = {};
|
|
131
|
-
for (const row of rows) {
|
|
132
|
-
const key = String(row?.key ?? "");
|
|
133
|
-
if (!SETTING_DEFAULTS[key]) continue;
|
|
134
|
-
try {
|
|
135
|
-
stored[key] = JSON.parse(String(row.value));
|
|
136
|
-
} catch {
|
|
137
|
-
/* Never fails the read: the defaults stand for that section, and the log says why. */
|
|
138
|
-
logger.warn(`[fina] FINA$SETTING "${key}" is not JSON; its defaults are used until it is saved again.`);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
return Object.fromEntries(Object.keys(SETTING_DEFAULTS).map((key) => [key, lay(SETTING_DEFAULTS[key], stored[key])]));
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
const write = async (patch, actorId) => {
|
|
145
|
-
const current = await read();
|
|
146
|
-
const checked = validateSettings(patch, current);
|
|
147
|
-
if (!checked.ok) throw finaError("INVALID", checked.problems.join("; "));
|
|
148
|
-
const statements = Object.entries(checked.value).map(
|
|
149
|
-
([key, value]) =>
|
|
150
|
-
`INSERT INTO FINA$SETTING (KEY, VALUE, UPDATED_BY, UPDATED_AT) VALUES (${quote(key)}, ${quote(JSON.stringify(value))}, ${actorSql(actorId)}, now()) ` +
|
|
151
|
-
"ON CONFLICT (KEY) DO UPDATE SET VALUE = EXCLUDED.VALUE, UPDATED_BY = EXCLUDED.UPDATED_BY, UPDATED_AT = now()",
|
|
152
|
-
);
|
|
153
|
-
if (statements.length > 0) await execStatements(statements);
|
|
154
|
-
/* What is now stored: the sections written, the others as they were read. */
|
|
155
|
-
return { ...current, ...checked.value };
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
return { read, write };
|
|
159
|
-
};
|
package/engine/fina/sql.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Doc 8 M3 — Firebird literals for the statements engine/fina builds (client.executeSql / client.block).
|
|
3
|
-
*
|
|
4
|
-
* The FINA API takes SQL TEXT: there is no parameter binding on this path. Every value that reaches a statement goes
|
|
5
|
-
* through here — a string quoted with its quotes doubled, a number as a finite number — so no value can carry SQL.
|
|
6
|
-
* ⚠️ Single-line on purpose: a raw newline ends the command on the FINA transport (client.js).
|
|
7
|
-
*/
|
|
8
|
-
import { finaError } from "./errors.js";
|
|
9
|
-
|
|
10
|
-
export const sqlString = (value) => `'${String(value ?? "").replace(/[\r\n]+/g, " ").replace(/'/g, "''")}'`;
|
|
11
|
-
|
|
12
|
-
export const sqlNumber = (value) => {
|
|
13
|
-
const n = Number(value);
|
|
14
|
-
if (value === null || value === undefined || value === "" || !Number.isFinite(n)) {
|
|
15
|
-
throw finaError("INVALID", `${JSON.stringify(value)} is not a number.`);
|
|
16
|
-
}
|
|
17
|
-
return String(n);
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export const sqlInteger = (value) => {
|
|
21
|
-
const n = Number(value);
|
|
22
|
-
if (value === null || value === undefined || value === "" || !Number.isInteger(n)) {
|
|
23
|
-
throw finaError("INVALID", `${JSON.stringify(value)} is not a whole number.`);
|
|
24
|
-
}
|
|
25
|
-
return String(n);
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
/* A value of a column of `type` ('string' or a number type), or NULL. */
|
|
29
|
-
export const sqlValue = (value, type) => {
|
|
30
|
-
if (value === null || value === undefined || value === "") return "NULL";
|
|
31
|
-
if (type === "string" || type === "date") return sqlString(value);
|
|
32
|
-
if (type === "integer" || type === "flag") return sqlInteger(value);
|
|
33
|
-
return sqlNumber(value);
|
|
34
|
-
};
|