bank20baht-validator 0.4.1 → 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.
Files changed (2) hide show
  1. package/dist/index.js +72 -18
  2. package/package.json +2 -2
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
- return validateField(descriptor, formData[field.key], registry, formData).map((err) => ({
422
- ref,
423
- message: err.message,
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
- function assertValidCondition(cond, refs, where) {
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
- if (typeof cond.field !== 'string' || !refs.has(cond.field)) {
831
- throw new Error(`${where} references unknown field ${JSON.stringify(cond.field)}`);
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
- const value_field = cond.value_field;
834
- if (value_field !== undefined && (typeof value_field !== 'string' || !refs.has(value_field))) {
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
- if (typeof expr.ref !== 'string' || !refs.has(expr.ref)) {
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.1",
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",
@@ -46,7 +46,7 @@
46
46
  "vitest": "^3.2.4"
47
47
  },
48
48
  "peerDependencies": {
49
- "bank20baht-ui": ">=0.11.0 <0.13.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",