bank20baht-validator 0.4.0 → 0.4.2
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/dist/index.js +72 -18
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// every value must have the shape the UI control could actually emit before
|
|
14
14
|
// the authored validators run.
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
|
-
import { applyFlags, approvalFields, createScriptContext, defaultValue, evaluate, hasScripts, runScripts, getCatalog, normalizeTagsValue, parseDisabledEntry, parseIsoDate, parseIsoDateTime, resolveViewFields, setCatalog, setLocale, t, validateField, walkPath, normalizeLegacySchema, } from 'bank20baht-ui/engine';
|
|
16
|
+
import { applyFlags, approvalFields, conditionRowRefs, createScriptContext, defaultValue, evaluate, hasScripts, runScripts, getCatalog, normalizeTagsValue, parseDisabledEntry, parseIsoDate, parseIsoDateTime, parseRowRef, resolveViewFields, setCatalog, setLocale, t, validateField, walkPath, normalizeLegacySchema, } from 'bank20baht-ui/engine';
|
|
17
17
|
/**
|
|
18
18
|
* Run `fn` with the catalog switched to `locale`, restoring the previous
|
|
19
19
|
* catalog after. The engine catalog is module-global; validation is fully
|
|
@@ -418,10 +418,24 @@ function workflowFieldIssues(form, field, derived, registry) {
|
|
|
418
418
|
formData[field.key] = normalizeCanonicalValue(field, formData[field.key]);
|
|
419
419
|
const required = state ? state.required : !!field.required;
|
|
420
420
|
const descriptor = required === !!field.required ? field : { ...field, required };
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
421
|
+
const rowStates = field.type === 'repeater' ? rowStatesOf(form, field, derived) : undefined;
|
|
422
|
+
return validateField(descriptor, formData[field.key], registry, formData, rowStates).map((err) => ({ ref: issueRef(form, field, err.key), message: err.message }));
|
|
423
|
+
}
|
|
424
|
+
/** A repeater's row errors keep their `key[i].sub` ref (spec 01 §10); the rest sit on the field. */
|
|
425
|
+
function issueRef(form, field, errKey) {
|
|
426
|
+
return errKey.startsWith(`${field.key}[`)
|
|
427
|
+
? `${form.form_name}.${errKey}`
|
|
428
|
+
: `${form.form_name}.${field.key}`;
|
|
429
|
+
}
|
|
430
|
+
/** The rule-derived states of one repeater's row sub-fields, keyed `key[i].sub` (spec 02 §2a). */
|
|
431
|
+
function rowStatesOf(form, field, derived) {
|
|
432
|
+
const prefix = `${form.form_name}.${field.key}[`;
|
|
433
|
+
const states = {};
|
|
434
|
+
for (const [ref, state] of Object.entries(derived.fields)) {
|
|
435
|
+
if (ref.startsWith(prefix))
|
|
436
|
+
states[ref.slice(form.form_name.length + 1)] = state;
|
|
437
|
+
}
|
|
438
|
+
return states;
|
|
425
439
|
}
|
|
426
440
|
function formFieldIssues(form, derived, registry) {
|
|
427
441
|
const issues = [];
|
|
@@ -600,7 +614,7 @@ function assertTransitions(wf, nameSet, refs) {
|
|
|
600
614
|
if (tr.to !== 'end' && !nameSet.has(tr.to)) {
|
|
601
615
|
throw new Error(`transition to unknown form ${JSON.stringify(tr.to)}`);
|
|
602
616
|
}
|
|
603
|
-
assertValidCondition(tr.when, refs, `transition ${tr.from} -> ${tr.to}
|
|
617
|
+
assertValidCondition(tr.when, refs, `transition ${tr.from} -> ${tr.to}`, false);
|
|
604
618
|
}
|
|
605
619
|
}
|
|
606
620
|
function assertValidWorkflow(parsed) {
|
|
@@ -626,10 +640,20 @@ function fieldRefs(wf) {
|
|
|
626
640
|
if (field.type === 'label')
|
|
627
641
|
continue;
|
|
628
642
|
refs.add(`${form.form_name}.${field.key}`);
|
|
643
|
+
for (const ref of rowRefsOf(form.form_name, field))
|
|
644
|
+
refs.add(ref);
|
|
629
645
|
}
|
|
630
646
|
}
|
|
631
647
|
return refs;
|
|
632
648
|
}
|
|
649
|
+
/** `Form.repeater[].sub`: the same-row references rules may use (spec 02 §2a). */
|
|
650
|
+
function rowRefsOf(form_name, field) {
|
|
651
|
+
if (field.type !== 'repeater')
|
|
652
|
+
return [];
|
|
653
|
+
return (field.fields ?? [])
|
|
654
|
+
.filter((sub) => sub.type !== 'label' && sub.type !== 'section' && sub.type !== 'repeater')
|
|
655
|
+
.map((sub) => `${form_name}.${field.key}[].${sub.key}`);
|
|
656
|
+
}
|
|
633
657
|
function assertSubFields(field, form_name) {
|
|
634
658
|
if (field.fields === undefined)
|
|
635
659
|
return;
|
|
@@ -783,6 +807,9 @@ function assertRuleAction(rawAction, where, formNames, refs) {
|
|
|
783
807
|
}
|
|
784
808
|
}
|
|
785
809
|
if (rawAction.action === 'setValue') {
|
|
810
|
+
if (parseRowRef(`${target.form}.${String(target.field)}`)) {
|
|
811
|
+
throw new Error(`${where}: setValue cannot target a repeater row field`);
|
|
812
|
+
}
|
|
786
813
|
assertValidExpr(rawAction.value, refs, where);
|
|
787
814
|
}
|
|
788
815
|
}
|
|
@@ -804,13 +831,38 @@ function assertRule(raw, i, formNames, refs) {
|
|
|
804
831
|
for (const rawAction of actions)
|
|
805
832
|
assertRuleAction(rawAction, where, formNames, refs);
|
|
806
833
|
}
|
|
834
|
+
assertSingleRowScope(rule, where);
|
|
835
|
+
}
|
|
836
|
+
/** A row-scoped rule is evaluated per row of ONE repeater (spec 02 §2a). */
|
|
837
|
+
function assertSingleRowScope(rule, where) {
|
|
838
|
+
const scoped = new Set(conditionRowRefs(rule.when).map((r) => `${r.form}.${r.repeater}`));
|
|
839
|
+
for (const branch of [rule.then, rule.else]) {
|
|
840
|
+
for (const action of Array.isArray(branch) ? branch : []) {
|
|
841
|
+
const row = actionRowRef(action);
|
|
842
|
+
if (row)
|
|
843
|
+
scoped.add(`${row.form}.${row.repeater}`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
if (scoped.size > 1)
|
|
847
|
+
throw new Error(`${where}: a rule may be row-scoped to one repeater only`);
|
|
848
|
+
}
|
|
849
|
+
/** The row-scoped ref an action targets, if any. */
|
|
850
|
+
function actionRowRef(action) {
|
|
851
|
+
const target = isRecord(action) && isRecord(action.target) ? action.target : undefined;
|
|
852
|
+
if (!target || typeof target.field !== 'string')
|
|
853
|
+
return null;
|
|
854
|
+
return parseRowRef(`${String(target.form)}.${target.field}`);
|
|
807
855
|
}
|
|
808
856
|
function assertValidRules(wf, formNames, refs) {
|
|
809
857
|
wf.rules.forEach((raw, i) => {
|
|
810
858
|
assertRule(raw, i, formNames, refs);
|
|
811
859
|
});
|
|
812
860
|
}
|
|
813
|
-
|
|
861
|
+
/**
|
|
862
|
+
* `allowRow`: a row-scoped ref (`Form.repeater[].sub`) only means something in
|
|
863
|
+
* a rule's `when` (spec 02 §2a) — transitions and value expressions have no row.
|
|
864
|
+
*/
|
|
865
|
+
function assertValidCondition(cond, refs, where, allowRow = true) {
|
|
814
866
|
if (cond === undefined)
|
|
815
867
|
return; // omitted = always true
|
|
816
868
|
if (!isRecord(cond))
|
|
@@ -820,28 +872,30 @@ function assertValidCondition(cond, refs, where) {
|
|
|
820
872
|
if (!Array.isArray(list))
|
|
821
873
|
throw new Error(`invalid condition in ${where}`);
|
|
822
874
|
for (const c of list)
|
|
823
|
-
assertValidCondition(c, refs, where);
|
|
875
|
+
assertValidCondition(c, refs, where, allowRow);
|
|
824
876
|
return;
|
|
825
877
|
}
|
|
826
878
|
if ('not' in cond) {
|
|
827
|
-
assertValidCondition(cond.not, refs, where);
|
|
879
|
+
assertValidCondition(cond.not, refs, where, allowRow);
|
|
828
880
|
return;
|
|
829
881
|
}
|
|
830
|
-
|
|
831
|
-
|
|
882
|
+
assertKnownRef(cond.field, refs, where, allowRow);
|
|
883
|
+
if (cond.value_field !== undefined)
|
|
884
|
+
assertKnownRef(cond.value_field, refs, where, allowRow);
|
|
885
|
+
}
|
|
886
|
+
function assertKnownRef(ref, refs, where, allowRow) {
|
|
887
|
+
if (typeof ref !== 'string' || !refs.has(ref)) {
|
|
888
|
+
throw new Error(`${where} references unknown field ${JSON.stringify(ref)}`);
|
|
832
889
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
throw new Error(`${where} references unknown field ${JSON.stringify(value_field)}`);
|
|
890
|
+
if (!allowRow && parseRowRef(ref)) {
|
|
891
|
+
throw new Error(`${where}: a row-scoped ref is only valid in a rule condition`);
|
|
836
892
|
}
|
|
837
893
|
}
|
|
838
894
|
function assertValidExpr(expr, refs, where) {
|
|
839
895
|
if (!isRecord(expr))
|
|
840
896
|
throw new Error(`${where}: invalid value expression`);
|
|
841
897
|
if ('ref' in expr) {
|
|
842
|
-
|
|
843
|
-
throw new Error(`${where} references unknown field ${JSON.stringify(expr.ref)}`);
|
|
844
|
-
}
|
|
898
|
+
assertKnownRef(expr.ref, refs, where, false);
|
|
845
899
|
return;
|
|
846
900
|
}
|
|
847
901
|
if ('const' in expr)
|
|
@@ -859,7 +913,7 @@ function assertValidExpr(expr, refs, where) {
|
|
|
859
913
|
if (!Array.isArray(triple) || triple.length !== 3) {
|
|
860
914
|
throw new Error(`${where}: if expression needs [condition, then, else]`);
|
|
861
915
|
}
|
|
862
|
-
assertValidCondition(triple[0], refs, where);
|
|
916
|
+
assertValidCondition(triple[0], refs, where, false);
|
|
863
917
|
assertValidExpr(triple[1], refs, where);
|
|
864
918
|
assertValidExpr(triple[2], refs, where);
|
|
865
919
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bank20baht-validator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Runtime validation for bahtui form/workflow JSON \u2014 the exact engine rules the client runs, on the server.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
"test:coverage": "vitest run --coverage"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"bank20baht-ui": "^0.
|
|
44
|
+
"bank20baht-ui": "^0.12.0",
|
|
45
45
|
"typescript": "^5.8.3",
|
|
46
46
|
"vitest": "^3.2.4"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"bank20baht-ui": ">=0.
|
|
49
|
+
"bank20baht-ui": ">=0.12.1 <0.13.0"
|
|
50
50
|
},
|
|
51
51
|
"main": "./dist/index.js",
|
|
52
52
|
"types": "./dist/index.d.ts",
|